<?php
/**
 * ClassScanner v0.0.2
 * 
 * Naar een idee van Richard van Velzen.
 * http://www.phphulp.nl/php/scripts/4/1604/
 *
 * Richard had een functie geschreven om zijn classen en interfaces te 
 * zoeken in de include paths. Dat bracht mij op het idee om er een class 
 * voor te schrijven.
 * 
 * Je kan er een directory met scannen al of niet recursief. Er is ook 
 * een functie die enkel één bestand scant. 
 * 
 * Ik heb ook een mini-api geschreven die de resultaten toont voor mijn 
 * localhost. Voor zij die er zin in hebben kunnen een formulier maken 
 * waar je de directory in opgeeft (opzoekt?!) en zo de resultaten laat
 * opzoeken.
 * 
 * Have fun.
 * 
 * @author Marc Cools 
 */

class THF_Tool_ClassScanner
{
	// Settings
	private $_directoryToScan;
	private $_treeScan;
	private $_extensions;

	// Results
	private $_scanFileResults;
	private $_scanDirectoryResults;
	private $_notReadableFiles;
	private $_elapsedTime;
	private $_peakMemory;

	// Magic functions
	
	public function __construct($directoryToScan=NULL, $treeScan=TRUE, $extensions=array('php','inc'))
	{
		$this->_directoryToScan 	= $directoryToScan;
		$this->_treeScan 			= $treeScan;
		$this->_extensions 			= $extensions;

		$this->_resetResults();
	}

	public function __set($name, $value)
	{
		switch( strtolower($name) )
		{
			case 'directory':
			case 'directorytoscan':
				$this->_directoryToScan = trim($value);
				break;
			case 'tree':
			case 'treescan':
				$this->_treeScan = (bool) $value;
				break;
			case 'extensions':
				if( NULL===$value ) $value = array();
				if( ! is_array($value) ) $value = array($value);
				$this->_extensions = $value;
				break;
			default:
				throw new InvalidArgumentException
				('Unknown name for __set methode.');
				break;
		}
	}

	public function __get($name)
	{
		switch( strtolower($name) )
		{
			case 'directory':
			case 'directorytoscan':
				return $this->_directoryToScan;
				break;
			case 'tree':
			case 'treescan':
				return $this->_treeScan;
				break;
			case 'extensions':
				return $this->_extensions;
				break;
			case 'fileresults':
			case 'scanfileresults':
				return $this->_scanFileResults;
				break;
			case 'results':
			case 'directoryresults':
			case 'scandirectoryresults':
				return $this->_scanDirectoryResults;
				break;
			case 'notreadable':
			case 'notreadablefiles':
				return $this->_notReadableFiles;
				break;
			case 'time':
			case 'elapsed':
			case 'elapsedtime':
				return $this->_elapsedTime;
				break;
			case 'bytes':
			case 'memory':
			case 'peakmemory':
				return $this->_peakMemory;
				break;
			default:
				throw new InvalidArgumentException
				("Unknown name for __get methode.");
				break;
		}

	}

	// Methods
	
	public function memoryString($megaString='M', $kiloString='K', $byteString='B')
	{
		if( ! isset($this->_peakMemory) ) return '';
		
		$mb = floor($this->_peakMemory /1024/1024);
		$kb = floor( ($this->_peakMemory - $mb*1024*1024 ) / 1024);
		$bt = $this->_peakMemory - $mb*1024*1024 - $kb*1024;
		
		$out = '';
		if( $mb > 0 ) $out = $mb . trim($megaString). ' ';
		if( $mb > 0 OR $kb > 0 ) $out .= $kb . trim($kiloString). ' ';
		$out .= $bt . trim($byteString);
		return trim($out);
	}
	
	public function scanFile($fileToScan, $extensions=NULL)
	{
		$fileToScan = realpath($fileToScan);

		if( ! is_readable($fileToScan) )
		{
			$this->_notReadableFiles[] = $fileToScan;
			$this->_scanFileResults = array();
			return $this->_scanFileResults;
		}
		
		if( NULL===$extensions ) $extensions = $this->_extensions;
		
		if( ! NULL===$extensions )
		{
			if( ! is_array($extensions) ) $extensions=array($extensions);
			$found = FALSE;
			foreach( $extensions as $ext )
			{
				$lenExt = strlen($ext);
				$lenFle = strlen($fileToScan);
				if( FALSE === strpos($fileToScan,$ext,$lenFle-$lenExt-1) ) continue;
				 
				$found = TRUE;
				break;
			}
			if( ! $found )
			{
				$this->_scanFileResults = array();
				return $this->_scanFileResults;
			}
		}
		 
		$foundNames = array();
	  
		$tokens = @token_get_all(file_get_contents($fileToScan));
	  
		for( $i=0, $length=count($tokens) ; $i !== $length ; ++$i )
		{
			if( is_array($tokens[$i])
				AND (	$tokens[$i][0] === T_CLASS
				OR $tokens[$i][0] === T_INTERFACE ) )
			{
				for( $j=$i+1 ; $j !== $length ; ++$j )
				{
					if( is_array($tokens[$j])
					AND $tokens[$j][0] == T_STRING )
					{
						$foundNames[] = $tokens[$j][1];
						break 1;
					}
				}
			}
		}
		$this->_scanFileResults = $foundNames;
		return $this->_scanFileResults;
	}

	public function scanDirectory($directory=NULL, $treeScan=NULL, $extensions=NULL)
	{
		$time = microtime(TRUE);
		$temp = $this->_scanFileResults;
		$this->_resetResults();
		
		if( NULL===$directory ) $directory=$this->_directoryToScan;
		if( NULL===$directory )
		{
			throw new exception
			('You need a directory to scan.');
			return FALSE;
		}
		
		if( NULL===$treeScan ) $treeScan=$this->_treeScan;
		if( NULL===$treeScan ) $treeScan=TRUE;
		
		if( NULL===$extensions ) $extensions=$this->_extensions;
		
		$this->_scanDirectoryResults = $this->_scanDirectory($directory, $treeScan, $extensions);
		
		$this->_scanFileResults = $temp;
		$this->_peakMemory = memory_get_peak_usage(TRUE);
		$this->_elapsedTime = microtime(TRUE) - $time;
		
		return $this->_scanDirectoryResults;
	}

	// Helpers
	
	private function _scanDirectory($directory, $treeScan=NULL, $extensions=NULL)
	{
		$classes = array();

		// OS specifieke directory separator gebruiken
		$directory = str_replace('/', DIRECTORY_SEPARATOR, $directory);
		$directory = str_replace('\\', DIRECTORY_SEPARATOR, $directory);
		if( substr($directory, - 1, 1) !== DIRECTORY_SEPARATOR )
		{
			$directory .= DIRECTORY_SEPARATOR;
		}
		$dir = new DirectoryIterator($directory);
		foreach( $dir as $subPart )
		{
			if( $subPart->isFile() )
			{
				$results = $this->scanFile($subPart->getPathname(), $extensions);
				if( ! empty($results) )
				{
					$classes[] = array(
	    				'file'	=>	$subPart->getPathname(),
	    				'found'	=>	$results
					);
				}
			}
			elseif( $treeScan 
					AND $subPart->isDir() 
					AND ! $subPart->isDot() 
					AND (FALSE === strpos($subPart->getBasename(),'.')) )
			{
				$classes = array_merge($classes, $this->_scanDirectory($subPart->getPathname(), $treeScan, $extensions));
			}
		}
		unset($dir);
		return $classes;
	}

	private function _resetResults()
	{
		$this->_scanFileResults = array();
		$this->_scanDirectoryResults = array();
		$this->_notReadableFiles = array();
		$this->_elapsedTime = 0;
		$this->_peakMemory = 0;
	}

}




class THF_Tool_ClassScanner_API
extends THF_Tool_ClassScanner
{
	public function __construct()
	{
		parent::__construct();
	}

	public function localhost()
	{
		$directory = 'C:/xampp/htdocs';
		$this->scanDirectory($directory);
	}

	public function htmlResults()
	{
		echo '<h1>Scanning for Class and interface definitions.</h1>';
		echo '<p><i>Needed ' . $this->time . ' seconds and ' . $this->memoryString() . ' memory.</i></p><hr />';
		echo '<table border="1">';
		echo '<tr><th>file</th><th>class/interface</th></tr>';
		foreach( $this->results as $oneResult )
		{
			foreach( $oneResult['found'] as $class )
			{
				echo '<tr><td>'.$oneResult['file'].'</td><td>'.$class.'</td></tr>';
			}
		}
		echo '</table><br />';

		if( empty($this->notReadable) ) return TRUE;

		echo '<table border="1">';
		echo '<tr><th>Could not read this files:</th></tr>';
		foreach( $this->notReadable as $filename )
		{
			echo '<tr><td>'.$filename.'</td></tr>';
		}
		echo "</table>";
	}
}


// main 

$obj = new THF_Tool_ClassScanner_API();
$obj->localhost();
$obj->htmlResults();
?>
