A static analysis tool that bridges legacy procedural PHP to modern Symfony / PSR-standard code using AST traversal and local LLM-assisted refactoring.
Enterprise PHP codebases written in the early 2000s share a common fingerprint:
// ❌ The enemy
global $db_connection;
$result = mysql_query("SELECT * FROM users WHERE id = $id", $db_connection);These patterns — global variables, mysql_* calls, God Functions with cyclomatic complexity in the 30s — are not just ugly. They are actively dangerous: untestable, SQL-injection-prone, and impossible to migrate to modern frameworks without a systematic approach.
OpenLegacyGuard automates the detection step and bridges the gap to the refactor step using a local LLM.
┌──────────────┐ ┌──────────────────┐ ┌─────────────────────┐ ┌─────────────┐
│ PHP Source │────▶│ nikic/php-parser │────▶│ NodeTraverser │────▶│ ScanReport │
│ Files │ │ (AST generation) │ │ + Rule Visitors │ │ Violations │
└──────────────┘ └──────────────────┘ └─────────────────────┘ └──────┬──────┘
│
┌─────────────▼──────────────┐
│ ConsoleReporter │
│ (Colored table output) │
└─────────────┬──────────────┘
│ --refactor
┌─────────────▼──────────────┐
│ AiRefactorService │
│ (Ollama HTTP API) │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ DiffRenderer │
│ (Legacy RED / Modern GREEN│
└────────────────────────────┘
Why the Visitor Pattern?
nikic/php-parser's NodeTraverser accepts multiple NodeVisitor instances and dispatches every node to all of them in a single O(n) pass. Each rule is a self-contained visitor class — adding a new rule is a three-line change with zero impact on existing rules. This is the Open/Closed Principle in action.
Requirements: PHP 8.3+, Composer, Ollama (for --refactor mode)
git clone <repo>
cd OpenLegacyGuard
composer install
chmod +x bin/guardInstall Ollama (for AI refactoring):
# macOS / Linux
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull llama3
ollama serve./bin/guard scan /var/www/legacy/srcSample output:
OpenLegacyGuard — scanning /var/www/legacy/src
12/12 [============================] 100% — Done
3 smell(s) found across 2 file(s)
src/helpers/user.php
Line Rule Message
12 Global Variable Usage Use of `global` keyword detected ($db_connection, $current_user)...
18 Global Variable Usage Use of `global` keyword detected ($db_connection)...
src/controllers/order.php
48 High Complexity / God Fn Function `processOrder()` is a God Function: cyclomatic complexity 22...
./bin/guard scan /var/www/legacy/src --refactorAfter the scan, you are prompted to select a violation:
Select a violation to refactor (or "Exit"):
[1] Global Variable Usage — user.php:12
[2] Global Variable Usage — user.php:18
[3] High Complexity / God Function — order.php:48
[4] Exit
Press [1] and wait for Ollama:
──── LEGACY ────────────────────────────────────────────
− 1 │ global $db_connection, $current_user;
− 2 │ $result = mysql_query("SELECT * FROM users WHERE id = $id");
──── MODERN (Suggested Refactor) ──────────────────────
+ 1 │ <?php
+ 2 │ final class UserRepository extends ServiceEntityRepository
+ 3 │ {
+ 4 │ public function __construct(ManagerRegistry $registry)
+ 5 │ {
+ 6 │ parent::__construct($registry, User::class);
+ 7 │ }
...
./bin/guard scan src/ --no-progress
echo $? # 0 = clean, 1 = violations foundsrc/
├── Ai/
│ ├── AiRefactorException.php # Base exception for all Ollama errors
│ ├── AiRefactorService.php # HTTP client wrapper for Ollama API
│ ├── OllamaTimeoutException.php # LLM inference timeout
│ ├── OllamaUnavailableException.php # Ollama not running
│ └── RefactorPromptBuilder.php # Builds rule-specific system + user prompts
├── Analyzer/
│ └── AstAnalyzer.php # Parses files → traverses AST → stamps violations
├── Command/
│ └── ScanCommand.php # Symfony Console command: scan + interactive refactor
├── Diff/
│ └── DiffRenderer.php # Colored Legacy/Modern diff in terminal
├── Report/
│ ├── ConsoleReporter.php # Renders ScanReport to Symfony Console
│ ├── ScanReport.php # Aggregates Violations; groupByFile/groupByRule
│ └── Violation.php # Immutable value object: ruleName/filePath/line/message
├── Rules/
│ ├── AbstractRule.php # NodeVisitorAbstract base; addViolation() helper
│ ├── ComplexityDetector.php # Flags God Functions by line count + cyclomatic complexity
│ ├── GlobalUsageDetector.php # Detects `global` keyword usage
│ ├── ProceduralSqlDetector.php # Detects mysql_*, raw PDO->query(), raw SQL strings
│ ├── RuleInterface.php # Contract: getName/getDescription/getViolations/reset
│ └── RulesRegistry.php # Central registry: RulesRegistry::all()
└── Scanner/
└── FileScanner.php # Symfony Finder wrapper; recursive .php discovery
- Create
src/Rules/YourNewDetector.php:
<?php
declare(strict_types=1);
namespace OpenLegacyGuard\Rules;
use PhpParser\Node;
final class YourNewDetector extends AbstractRule
{
public function getName(): string { return 'Your Rule Name'; }
public function getDescription(): string { return 'What it detects.'; }
public function enterNode(Node $node): null
{
if ($node instanceof Node\Stmt\Echo_) {
$this->addViolation(
line: (int) $node->getStartLine(),
message: 'Use of echo in non-template context.',
);
}
return null;
}
}- Register in
src/Rules/RulesRegistry.php:
return [
new GlobalUsageDetector(),
new ProceduralSqlDetector(),
new ComplexityDetector(),
new YourNewDetector(), // ← add here
];- Write a PHPUnit test:
touch tests/Rules/YourNewDetectorTest.php
./vendor/bin/phpunit tests/Rules/YourNewDetectorTest.phpDone. Zero changes to existing code.
composer install
./vendor/bin/phpunitTest coverage includes:
- Unit tests for each rule against dedicated fixture files
- Negative tests —
clean_example.phpmust produce zero violations - Reset tests — rules must not accumulate state across files
- Integration tests — full scanner → analyzer → report pipeline
- AI service tests — mock HTTP client, all failure modes exercised
| Rule | What it detects | Modern alternative |
|---|---|---|
GlobalUsageDetector |
global $var inside functions |
Constructor injection / Symfony Service |
ProceduralSqlDetector |
mysql_*, raw PDO->query(), raw SQL strings |
Doctrine ORM / PDO prepared statements |
ComplexityDetector |
Functions > 50 lines or cyclomatic complexity > 15 | Single-responsibility methods / Services |
| Variable | Default | Description |
|---|---|---|
| Base URL | http://localhost:11434 |
Change if Ollama runs on a different host/port |
| Model | llama3 |
Any model pulled via ollama pull <model> |
| Timeout | 120s |
Increase for large snippets on CPU-only machines |
| Code | Meaning |
|---|---|
0 |
No violations found — codebase is clean |
1 |
One or more violations detected |
This makes guard scan safe to use as a CI gate.
MIT