Santaji GadeDevelopment, PHPYesterday5 Views

Build a redirect manager in PHP that replaces a bloated htaccess file with an indexed MySQL table, wildcard and regex matching, chain detection, and hit logging.
Table of Contents
ToggleA site migration that generates three thousand old URLs does not belong in a growing .htaccess file, since every added rule slows down every single request on the server. A redirect manager in PHP moves that lookup into a database where it belongs.
Apache reads an .htaccess file from top to bottom on every request, checking each rule in order until one matches.
A file with a few dozen redirect rules barely registers. A file with several thousand rules, built up after two or three site migrations, adds real overhead to every page load.
It also becomes almost impossible to search, edit safely, or hand off to someone else on the team without breaking something.
A small PHP script backed by an indexed database table replaces all of that with a single fast lookup, no matter how many redirects the table holds.
A single table covers most sites, as long as the source path column is indexed for fast lookups.
CREATE TABLE redirects (
id INT AUTO_INCREMENT PRIMARY KEY,
source_path VARCHAR(500) NOT NULL,
destination_url VARCHAR(500) NOT NULL,
status_code SMALLINT DEFAULT 301,
is_regex BOOLEAN DEFAULT 0,
hit_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY idx_source (source_path)
);
The unique key on source_path does double duty. It keeps the lookup fast and stops the same old URL from ever being mapped to two different destinations by mistake. MySQL's own indexing guide explains why this matters more as a table grows past a few thousand rows.
PDO with prepared statements keeps the lookup safe from injection, even though the only input here is the requested path itself.
$pdo = new PDO(
'mysql:host=localhost;dbname=brandella;charset=utf8mb4',
getenv('DB_USER'), getenv('DB_PASS')
);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
function find_redirect(PDO $pdo, $path) {
$stmt = $pdo->prepare(
'SELECT * FROM redirects WHERE source_path = ? AND is_regex = 0 LIMIT 1'
);
$stmt->execute([$path]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
This function only checks exact matches for now. The next two sections add wildcard and regex support on top of it.
The redirect check needs to run as early as possible in the request lifecycle, before the page template or any heavy application code loads.
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$redirect = find_redirect($pdo, $path);
if ($redirect) {
log_redirect_hit($pdo, $redirect['id']);
header('Location: ' . $redirect['destination_url'], true, $redirect['status_code']);
exit;
}
Passing the status code from the database into header() means the same function handles both a 301 permanent move and a 302 temporary redirect without any extra branching.
Not every redirect maps one old URL to one new URL. A whole folder of old product pages often needs to collapse into a single category page.
function find_regex_redirect(PDO $pdo, $path) {
$stmt = $pdo->query(
'SELECT * FROM redirects WHERE is_regex = 1'
);
foreach ($stmt as $rule) {
if (preg_match($rule['source_path'], $path, $matches)) {
return [
'id' => $rule['id'],
'destination_url' => preg_replace($rule['source_path'], $rule['destination_url'], $path),
'status_code' => $rule['status_code']
];
}
}
return null;
}
Regex rules are checked only after the exact match table lookup fails, since scanning every regex row on every single request would erase the speed advantage the exact match index provides.
| Matching Strategy | Speed | Best For |
|---|---|---|
| Exact path lookup | Fastest, uses the index directly | Individual page moves |
| Wildcard prefix match | Fast, small candidate set | Whole folders moving together |
| Regex pattern match | Slower, checked as a fallback | Complex URL structure changes |
A chain forms when one redirect points at a URL that is itself another redirect, and Google's own redirect guidance flags long chains as a real crawl efficiency problem.
function creates_chain(PDO $pdo, $destination) {
$path = parse_url($destination, PHP_URL_PATH);
$stmt = $pdo->prepare('SELECT id FROM redirects WHERE source_path = ?');
$stmt->execute([$path]);
return $stmt->fetch() !== false;
}
Running this check before saving a new redirect catches the chain at creation time, which is far easier than untangling one after it has been live for months.
Not every redirect stays useful forever. Some stop receiving any traffic once old backlinks and bookmarks age out, a pattern Search Engine Journal's redirect guide covers in more depth from the SEO side of things.
function log_redirect_hit(PDO $pdo, $id) {
$stmt = $pdo->prepare(
'UPDATE redirects SET hit_count = hit_count + 1 WHERE id = ?'
);
$stmt->execute([$id]);
}
A quarterly query for rows where hit_count stayed at zero surfaces redirects nobody is actually using anymore, safe candidates for cleanup. This kind of ongoing maintenance is exactly what a redirect manager in PHP makes practical at a scale .htaccess never could.
Traversy Media, referenced earlier in this series for a working PHP contact form, also covers the database fetching pattern that a redirect lookup builds directly on.
Video credit: Traversy Media.
Swapping the query shown in the video for the prepared statement lookup in this guide is the entire difference between a demo and a production ready redirect manager in PHP.
Well into the hundreds of thousands, since an indexed exact match lookup stays fast regardless of table size, unlike a growing .htaccess file that Apache reads line by line. Watching crawl stats in Search Console is a good way to confirm the migration went smoothly.
Performance at scale, easier editing through an admin interface, built in chain detection, and hit logging that shows which redirects are still actually needed.
Only permanent moves should. A temporary change, like a seasonal page, is a better fit for a 302, which the status_code column in this design already supports.
The exact match table always takes priority in this design, and regex rules are only checked once the exact match lookup comes back empty.
Checking at creation time, as shown in this guide, prevents most chains before they exist, so a full audit is really only needed after a large migration.
Thousands of rules read line by line add real overhead to every request.
A unique key on source_path keeps performance flat as the table grows.
Checking regex rules only as a fallback protects the fast path.
A check at creation time beats untangling a chain months later.
Zero hit redirects are safe, low risk candidates for cleanup.
301 for permanent moves, 302 for anything meant to be temporary.
Pair a redirect manager in PHP with a wider toolkit to keep every site migration clean and every old URL accounted for.









