Поиск места декларирования функции средствами PHP

в 9:48, , рубрики: function, php, Песочница, метки: ,

Где функция?Шастая по строкам кода одного из своих проектов, я наткнулся на функцию, значение которой я давно забыл. Проект хоть и имеет в качестве основы cms, но создателя этой функции я с точностью определил себя. Ее название выбивалось из общего стиля. Тем неприятнее для меня было осознавать, что я не помню, что она делает, и не знаю где она продекларирована, чтобы по ее коду определить. Проверив список из нескольких вероятных мест декларирования, я понял, что функция запрятана в нестандартном месте, мне вспомнилось несколько трогательных моментов связанных с тем далеким временем, когда я вел структуру проекта очень безответственно. Когда сеанс ностальгии прошел, проблема с нахождением места декларирования функции осталась.
К сожалению, применить великолепный метод grep-ания по файлам проекта не подходил изза отсутствия ssh. Список возможных методов решения сужался к одному очень неприятному «поочередная проверка подключаемых файлов». Но госпожа лень взяла вверх, а, как известно именно она является основным двигателем прогресса.

Я подумал, что вместо того чтобы в ручную проверять подключенные файлы, можно написать скрипт который это будет делать автоматически и выдавать файл и строку где находится декларирование функции. Немного позже я подумал, что узнать это место можно было более простым и топорным методом, сгенерировав ошибку дублированного декларирования, создавая функцию с таким же именем по ошибке можно узнать место, где эта функция уже существует. Этот метод неудобен на работающих проектах, но и тут можно исхитрится спрятав дубликат декларирования в проверку, например по ip.

Задача мне показалась довольно интересной, а в мозгу уже был алгоритм, который ждал воплощения. Что нужно сделать? Нужно сделать функцию, которая будет искать во всех подключенных, на момент ее вызова, файлах определение функции, имя которой задается в качестве параметра.

Для начала обхода файлов используются данные от функции debug_backtrace() которая возвращает основные файлы. Следующий этап это запуск рекурсивной функции, которая просматривает все файлы подключающиеся к процессу.

<?

// Список функции которые подключают дополнительные файлы 
$INC_FUNC=array("include","require","include_once","require_once");
// Список файлов которые были уже обработанны
$FILES_SEARCH=array();

function find_declaration($fname){
	global $INC_FUNC,$TRACE_ROOT_PATH;
	
	$trace=debug_backtrace();

	// Находим корень запуска 
	$TRACE_ROOT_PATH=$trace[sizeof($trace)-1]['file'];
	$TRACE_ROOT_PATH=pathinfo($TRACE_ROOT_PATH);
	$TRACE_ROOT_PATH=$TRACE_ROOT_PATH['dirname'];
	
	
	//для каждого уровня вложения из результатов debug_backtrace проверяем файлы
	foreach($trace as $n=>$v) {
		if($n==0 or array_search($v['function'],$INC_FUNC)!==false)
			$srez=scan_file($fname,$v['file']);	// Запуск рекурсивного поиска по файлу
		if($srez!==false) break;
	}
	
	return $srez;
}


function scan_file($fname,$file){
	
	global $INC_FUNC,$TRACE_ROOT_PATH,$FILES_SEARCH;
	
	$cont=file_get_contents($file);
	$cont_or=$cont;
	
	// Удаляем коментарии из текста скрипта 
	$cont=preg_replace("#/*.**/#Uis","",$cont);
	$cont=preg_replace("#//.*n#U","",$cont);
	$cont=preg_replace("'#.*n'U","",$cont);

	// Поиск формата декларирования
	preg_match("#functions+".$fname.".*n#i",$cont,$m);

	if(sizeof($m)>0){
		$cont_or=substr($cont_or,0,strpos($cont_or,$m[0]));
		$mline=explode("n",$cont_or);
		return array("file"=>$file,"line"=>sizeof($mline));
	} 
	
	// Поиск других подключенных файлов 
	preg_match_all("#(".implode("|",$INC_FUNC).")s+([^;$]+);#is",$cont,$minc);	

	foreach($minc[2] as $n=>$v ) {

		eval(" $v = ".$v.";");
		$inc_file="";
		if(is_file($v)){
			$inc_file=$v;
		} else 
		if(strrpos($file,"/")!==false) 
			$inc_file=substr($file,0,strrpos($file,"/"))."/".$v;
		else 
			$inc_file=substr($file,0,strrpos($file,"\"))."\".$v;
		
		if(!is_file($inc_file)) {
			$inc_file=$TRACE_ROOT_PATH."/".$v;
		} 

		// Поиск по вложеным файлам
		if(array_search($inc_file,$FILES_SEARCH)===false) {
			$FILES_SEARCH[]=$inc_file;
			$srez=scan_file($fname,$inc_file);
		} else $srez=false;
		
		if($srez!==false)
		return $srez;
	
	}

	return false;
}



$funcp=find_declaration('init');

echo "Функция init() находится в файле ".$funcp['file'].", строка ".$funcp['line'];

?>

В процессе пришлось перейти от простого копирования текста после функции типа include к преобразованию через функцию eval. Так воспринимаются и пути к подключенным файлам, содержащие константы или глобальные переменные, что значительно увеличивает стабильность работы скрипта. Есть ситуации, при которых функция не будет найдена, это когда путь к файлу определен через локальные переменные или другие конструкции не доступные из внешней среды.

Конечно, функционал напрашивается быть переделанным в ООП, но это уже дополнения. Для меня на этом этапе главное алгоритм.
Скрипт можно использовать как дополнение ко всяким дебагерам написанных на PHP.

Автор: Vencendor

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js