/** * PHP File Scanner * Scans directories for PHP files and reads their content */ import { glob } from 'glob'; import { readFile, stat } from 'fs/promises'; import { resolve, basename, dirname } from 'path'; /** * Represents a scanned PHP file */ export interface PhpFile { path: string; name: string; dir: string; content: string; lines: number; } /** * Represents extracted class information */ export interface ClassInfo { namespace: string | null; className: string | null; methods: MethodInfo[]; properties: PropertyInfo[]; uses: string[]; } /** * Represents a method in a PHP class */ export interface MethodInfo { visibility: string; name: string; parameters: string; returnType: string | null; } /** * Represents a property in a PHP class */ export interface PropertyInfo { visibility: string; type: string | null; name: string; } /** * Scanner options */ export interface ScanOptions { ignore?: string[]; } /** * Scan a directory or file path for PHP files * @param targetPath - Path to scan (file or directory) * @param options - Scan options * @returns Array of PHP files with their content */ export async function scanPhpFiles( targetPath: string, options: ScanOptions = {} ): Promise { const absolutePath = resolve(targetPath); const ignore = options.ignore || ['**/vendor/**', '**/node_modules/**']; // Check if path is a file or directory const pathStat = await stat(absolutePath); let files: string[] = []; if (pathStat.isFile()) { // Single file if (absolutePath.endsWith('.php')) { files = [absolutePath]; } else { throw new Error(`File ${absolutePath} is not a PHP file`); } } else if (pathStat.isDirectory()) { // Directory - scan for PHP files const pattern = `${absolutePath}/**/*.php`; files = await glob(pattern, { ignore }); } else { throw new Error(`Path ${absolutePath} is not a file or directory`); } // Read content of each file const results = await Promise.all( files.map(async (filePath) => { const content = await readFile(filePath, 'utf-8'); return { path: filePath, name: basename(filePath), dir: dirname(filePath), content, lines: content.split('\n').length, }; }) ); return results; } /** * Extract class information from PHP content * @param content - PHP file content * @returns Extracted class info */ export function extractClassInfo(content: string): ClassInfo { const info: ClassInfo = { namespace: null, className: null, methods: [], properties: [], uses: [], }; // Extract namespace const namespaceMatch = content.match(/namespace\s+([\w\\]+);/); if (namespaceMatch) { info.namespace = namespaceMatch[1]; } // Extract class name const classMatch = content.match( /class\s+(\w+)(?:\s+extends\s+\w+)?(?:\s+implements\s+[\w,\s]+)?/ ); if (classMatch) { info.className = classMatch[1]; } // Extract use statements const useMatches = content.matchAll(/use\s+([\w\\]+)(?:\s+as\s+\w+)?;/g); for (const match of useMatches) { info.uses.push(match[1]); } // Extract methods (public, protected, private) const methodMatches = content.matchAll( /(public|protected|private)\s+(?:static\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([\w|\\?]+))?/g ); for (const match of methodMatches) { info.methods.push({ visibility: match[1], name: match[2], parameters: match[3].trim(), returnType: match[4] || null, }); } // Extract properties const propMatches = content.matchAll( /(public|protected|private)\s+(?:readonly\s+)?(?:static\s+)?(?:([\w|\\?]+)\s+)?\$(\w+)/g ); for (const match of propMatches) { info.properties.push({ visibility: match[1], type: match[2] || null, name: match[3], }); } return info; }