Refactoring With AI

Turn messy, hard-to-maintain code into clean, structured code - without changing behavior. Every type of refactoring with exact prompt templates.

Intermediate 12 min read Lesson 14 of 20

Refactor Safely

Refactoring means changing the structure of code without changing its behavior. AI makes refactoring fast but introduces one new risk: it may accidentally change behavior while restructuring. Prevent this with two habits:

Safe refactoring protocol
Step 1 - Write tests FIRST (if none exist):
> Read UserModel::create(). Write PHPUnit tests for the current behavior
  (what inputs produce what outputs) BEFORE I refactor anything.

Step 2 - Refactor:
> Now refactor create() to extract validation to a private validateData() method.
  The observable behavior (what create() returns for any input) must not change.

Step 3 - Verify:
> Run the tests. Did they all pass? If not, what changed?

"Do not change the behavior" should appear in every refactoring prompt.

Extract and Split Patterns

Extract to method
> Read UserModel::create(). It is 80 lines long.
  Extract the validation logic (lines 12-35) to a private method validateUserData(array $data): array
  that returns ['valid' => bool, 'errors' => array].
  create() should call validateUserData() and use its result.
  Do not change the behavior of create() from the caller's perspective.
Extract to service class
> Read AuthController.php. The processLogin() method handles:
  validation, user lookup, password verify, session creation, remember_me, logging.
  That is too many responsibilities.

  Extract to app/services/AuthService.php:
  - authenticate(string $email, string $password): ?array  (returns user or null)
  - startSession(array $user, bool $remember): void
  - logout(): void

  Update AuthController to use AuthService.
  AuthController should have no direct PDO or session_start() calls after refactor.
Split a large file
> Read functions.php. It has 400 lines with unrelated utilities mixed together.
  Group the functions by category and split into separate files:
  - app/helpers/StringHelper.php  (string manipulation functions)
  - app/helpers/DateHelper.php    (date/time functions)
  - app/helpers/FileHelper.php    (file/image functions)
  - app/helpers/ValidationHelper.php (form validation functions)

  Create an autoloader or require-all file so existing code that uses
  these functions does not need to change its requires.

Apply Design Patterns

Repository pattern
> Read UserModel.php. Introduce the Repository pattern:
  - Create an interface: app/repositories/UserRepositoryInterface.php
    with methods: findById, findByEmail, create, update, delete
  - Create: app/repositories/UserRepository.php implementing the interface (PDO implementation)
  - Keep UserModel as a simple data object (no database code)

  This allows swapping the database implementation without changing business logic.
  Show me the refactored files and how to update the DI in controllers.
Strategy pattern
> Read PaymentController.php. It has a giant if/elseif block for
  Razorpay, Stripe, and PayPal with 100+ lines each.

  Apply the Strategy pattern:
  - Interface: app/payments/PaymentGatewayInterface.php
    with methods: createOrder(), verifyPayment(), refund()
  - Implement: app/payments/RazorpayGateway.php, StripeGateway.php, PayPalGateway.php
  - PaymentController uses $this->gateway->createOrder() only
  - Gateway is injected via constructor based on config setting

Clean Legacy Code

Legacy code cleanup prompts
# Remove global variables:
> Read the codebase. Find all uses of global $db, global $config.
  Replace with dependency injection (pass PDO/config as constructor parameter).
  Update all classes and the entry point file.

# Fix procedural to OOP:
> Read /pages/products.php. It is 200 lines of procedural PHP
  (no classes, direct $_GET/$_POST, inline SQL).
  Refactor to: ProductController class + ProductModel class + view template.
  Keep the same URL and same database structure.

# Remove copy-paste duplicates:
> I have UserController, ProductController, and OrderController.
  Each has nearly identical pagination code (80 lines).
  Extract it to a PaginationTrait that all three can use.

# Remove dead code:
> Read all PHP files. Find functions and methods that are never called.
  List them (do not delete yet - I will review the list first).

Modernize PHP Code

PHP 8+ modernization prompts
# Add type hints:
> Read app/models/UserModel.php. Add PHP 8.2 type declarations to all
  method parameters and return types. Use union types, nullable types,
  and built-in types (int, string, array, bool). Add 'strict_types=1'.

# Use named arguments:
> Read all files that call the UserModel constructor and DB methods.
  Replace positional arguments with named arguments where it improves clarity.
  Example: findUsers(limit: 20, offset: 0) instead of findUsers(20, 0)

# Use match expressions:
> Read any switch/case statements in the codebase.
  Replace them with PHP 8 match expressions where appropriate.
  List files changed.

# Use enums:
> The database has ENUM columns: status ('draft','published','archived'),
  priority ('low','medium','high','urgent').
  Create PHP 8.1 enums for these in app/enums/ and update models
  to use them instead of string comparisons.

# Use constructor property promotion:
> Read all model classes that have a __construct that assigns parameters to properties.
  Use PHP 8.0 constructor property promotion to shorten them.
  Example: public function __construct(private PDO $pdo) {}

Frequently Asked Questions

No - this is called "refactoring while changing behavior" and it multiplies bugs. Refactor first (tests should still pass), commit. Then add features. This way if something breaks after adding the feature, you know the refactoring was fine.

Run existing tests before and after. If no tests exist, ask the AI to write tests for the current behavior before refactoring, then verify those tests still pass after. Tests are your safety net - refactoring without them is flying blind.