├── .gitignore ├── CodeDependency.php ├── README.md ├── config_sample.php ├── example ├── function.inc.php └── test.php ├── get-env-functions.php ├── scan-dependencies.php └── test └── CodeDependencyTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /config.php 2 | -------------------------------------------------------------------------------- /CodeDependency.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class CodeDependency { 12 | const php_file_match = '/^.+\.php|.+\.inc$/i'; 13 | 14 | /** 15 | * record ALL function calls found 16 | * 17 | * @var array 18 | */ 19 | private $found_functions; 20 | 21 | /** 22 | * record ALL function definitions found 23 | * 24 | * @var array 25 | */ 26 | private $defined_custom_funcs; 27 | 28 | /** 29 | * record ALL classes instantiated 30 | * 31 | * @var array 32 | */ 33 | private $found_classes; 34 | 35 | private $buffer_start; 36 | private $current_func; 37 | private $function_start; 38 | private $new_operator; 39 | private $method_call; 40 | 41 | public function __construct() { 42 | $this->found_functions = array(); 43 | $this->defined_custom_funcs = array(); 44 | $this->found_classes = array(); 45 | } 46 | 47 | public function findDependenciesByDirectory( $all_functions, $dir ) { 48 | $Regex = new RegexIterator( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) ), 49 | self::php_file_match, 50 | RecursiveRegexIterator::GET_MATCH ); 51 | 52 | foreach ( $Regex as $r ) { 53 | $this->buffer_start = false; 54 | $this->current_func = ''; 55 | $this->function_start = false; 56 | $this->new_operator = false; 57 | $this->method_call = false; 58 | 59 | foreach ( token_get_all( file_get_contents( $r[0] ) ) as $token ) { 60 | if ( ! is_string( $token ) ) { 61 | $this->processTokenArray( $token ); 62 | } 63 | else { 64 | $this->processTokenString( $token ); 65 | } 66 | } 67 | } 68 | } 69 | 70 | public function getFoundFunctions() { 71 | return $this->found_functions; 72 | } 73 | 74 | public function getDefinedCustomFuncs() { 75 | return $this->defined_custom_funcs; 76 | } 77 | 78 | public function getFoundClasses() { 79 | return $this->found_classes; 80 | } 81 | 82 | private function normalise_function_name( $func ) { 83 | return strtolower( $func ); 84 | } 85 | 86 | private function processTokenArray( $token ) { 87 | list ( $id, $text ) = $token; 88 | 89 | if ( $id == T_STRING ) { 90 | $this->buffer_start = true; 91 | $this->current_func = $text; 92 | 93 | if ( $this->function_start ) { 94 | $this->defined_custom_funcs[] = $this->normalise_function_name( $this->current_func ); 95 | $this->function_start = false; 96 | } 97 | } 98 | elseif ( $id == T_FUNCTION ) { 99 | $this->function_start = true; 100 | $this->new_operator = false; 101 | } 102 | elseif ( $id == T_NEW ) { 103 | $this->new_operator = true; 104 | } 105 | elseif ( $id == T_WHITESPACE ) { 106 | // ignore whitespace 107 | } 108 | elseif ( $id == T_OBJECT_OPERATOR ) { 109 | $this->method_call = true; 110 | } 111 | else { 112 | $this->buffer_start = false; 113 | $this->new_operator = false; 114 | $this->method_call = false; 115 | } 116 | } 117 | 118 | private function processTokenString( $token ) { 119 | if ( $token == '(' ) { 120 | if ( $this->buffer_start ) { 121 | if ( ! $this->new_operator && ! $this->method_call ) { 122 | // got function 123 | if ( ! in_array( $this->normalise_function_name( $this->current_func ), $this->found_functions ) ) { 124 | $this->found_functions[] = $this->normalise_function_name( $this->current_func ); 125 | } 126 | } 127 | else { 128 | // got object instantiation 129 | if ( ! in_array( $this->current_func , $this->found_classes ) ) { 130 | $this->found_classes[] = $this->current_func; 131 | } 132 | } 133 | 134 | $this->buffer_start = false; 135 | } 136 | } 137 | elseif ( $token == ')' ) { 138 | $this->buffer_start = false; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compare PHP environment against PHP source code for function dependencies. 2 | 3 | A recent post on reddit/PHP asked an interesting question: how can you determine the extension requirements of your PHP application in a programmatic way? And could you add such a check to a continuous integration environment to validate dependencies? 4 | 5 | I've come up with a relative simple solution, albeit with some caveats (see below). The process requires two distinct stages: 6 | 7 | 1. Gather a list of extensions and functions from a PHP environment 8 | 2. Scan PHP source code for function calls and flag up any that are not either a) available in the PHP environment or b) user defined. 9 | 10 | For stage 1, you must determine the functions that are defined in your PHP environment. This is done by running `get-env-functions.php` either on the command line, or from within your document root. This will create a config file in the directory defined by `CONFIG_PATH`. This config file will be used by the second script, `scan-dependencies.php`. 11 | 12 | `scan-dependencies.php` will scan through PHP source code defined by `SOURCE_PATH` and use the configuration file generated previously. After it finishes scanning, it will list all function calls made that are not defined in either PHP itself, or within the source directory. 13 | 14 | ## Example Run 15 | 16 | Create a configuration file 17 | 18 | $ cp config_sample.php config.php 19 | 20 | Getting details of your PHP environment 21 | 22 | $ php get-env-functions.php 23 | Found 1743 total functions in 61 extensions available in PHP. 24 | 25 | Scanning source code for dependencies 26 | 27 | $ php scan-dependencies.php 28 | Found 3 function calls and 1 object instantiations in your script. 29 | Function ps_setgray not defined. 30 | Extensions required: standard 31 | 32 | In this example, the function `ps_setgray` was called in a script but not defined anywhere. 33 | 34 | ## Caveats 35 | 36 | * Your source code and its dependencies must lie under one directory -- included/required files outside this directory are not scanned 37 | * As it stands, only _function_ dependencies are found. This means that class dependencies are not checked. 38 | Final thoughts 39 | 40 | This is by no means a complete solution, but I hope it is of some use. Please feel free to comment, or suggest improvements. 41 | -------------------------------------------------------------------------------- /config_sample.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | require_once 'config.php'; 8 | 9 | $extensions = get_loaded_extensions(); 10 | $function_extensions = array(); 11 | $all_functions = array(); 12 | 13 | foreach ( $extensions as $ext ) { 14 | $functions = get_extension_funcs( $ext ); 15 | 16 | if ( $functions ) { 17 | $all_functions = array_merge( $all_functions, $functions ); 18 | foreach ( $functions as $function) { 19 | $function_extensions[ $function ] = $ext; 20 | } 21 | } 22 | } 23 | 24 | echo "Found " . count( $all_functions ) . " total functions in " . count( $extensions ) . " extensions available in PHP.\n"; 25 | 26 | file_put_contents( FUNCTION_CACHE_PATH, serialize( $all_functions ) ); 27 | file_put_contents( EXTENSION_CACHE_PATH, serialize( $function_extensions ) ); 28 | -------------------------------------------------------------------------------- /scan-dependencies.php: -------------------------------------------------------------------------------- 1 | findDependenciesByDirectory( $all_functions, SOURCE_PATH ); 19 | 20 | echo "Found " . count( $cd->getFoundFunctions() ) . " function calls and " . count( $cd->getFoundClasses() ) . " object instantiations in your script.\n"; 21 | 22 | foreach( $cd->getFoundFunctions() as $func ) { 23 | if ( ! in_array( $func, $all_functions ) && 24 | ! in_array( $func, $cd->getDefinedCustomFuncs() ) ) { 25 | echo "Function $func not defined.\n"; 26 | } 27 | if (isset($function_extensions[$func])) { 28 | $extensions[$func] = $function_extensions[$func]; 29 | } 30 | } 31 | $required_extensions = array_unique(array_values($extensions)); 32 | print "Extensions required: "; 33 | foreach ($required_extensions as $ext) { 34 | print $ext. ' '; 35 | } 36 | print PHP_EOL; 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/CodeDependencyTest.php: -------------------------------------------------------------------------------- 1 | code_dependency = new CodeDependency(); 11 | } 12 | 13 | public function testObjectInstantiation() { 14 | $this->assertTrue( is_a( $this->code_dependency, 'CodeDependency' ) ); 15 | $this->assertInternalType( 'array', $this->code_dependency->getFoundFunctions() ); 16 | $this->assertInternalType( 'array', $this->code_dependency->getDefinedCustomFuncs() ); 17 | $this->assertInternalType( 'array', $this->code_dependency->getFoundClasses() ); 18 | } 19 | } 20 | --------------------------------------------------------------------------------