<p>How can I find any unused functions in a PHP project?</p>

<p>Are there features or APIs built into PHP that will allow me to analyse my codebase - for example <a href="http://ie.php.net/manual/en/language.oop5.reflection.php" rel="noreferrer">Reflection</a>, <a href="http://php.net/manual/en/function.token-get-all.php" rel="noreferrer"><code>token_get_all()</code></a>?</p>

<p>Are these APIs feature rich enough for me not to have to rely on a third party tool to perform this type of analysis?</p>

[{'answer_id': 14625, 'author': 'Stacey Richards', 'author_id': 1142, 'author_profile': 'https://Stackoverflow.com/users/1142', 'pm_score': 6, 'selected': True, 'text': '<p>Thanks Greg and Dave for the feedback. Wasn\'t quite what I was looking for, but I decided to put a bit of time into researching it and came up with this quick and dirty solution:</p>\n\n<pre><code>&lt;?php\n    $functions = array();\n    $path = "/path/to/my/php/project";\n    define_dir($path, $functions);\n    reference_dir($path, $functions);\n    echo\n        "&lt;table&gt;" .\n            "&lt;tr&gt;" .\n                "&lt;th&gt;Name&lt;/th&gt;" .\n                "&lt;th&gt;Defined&lt;/th&gt;" .\n                "&lt;th&gt;Referenced&lt;/th&gt;" .\n            "&lt;/tr&gt;";\n    foreach ($functions as $name =&gt; $value) {\n        echo\n            "&lt;tr&gt;" . \n                "&lt;td&gt;" . htmlentities($name) . "&lt;/td&gt;" .\n                "&lt;td&gt;" . (isset($value[0]) ? count($value[0]) : "-") . "&lt;/td&gt;" .\n                "&lt;td&gt;" . (isset($value[1]) ? count($value[1]) : "-") . "&lt;/td&gt;" .\n            "&lt;/tr&gt;";\n    }\n    echo "&lt;/table&gt;";\n    function define_dir($path, &amp;$functions) {\n        if ($dir = opendir($path)) {\n            while (($file = readdir($dir)) !== false) {\n                if (substr($file, 0, 1) == ".") continue;\n                if (is_dir($path . "/" . $file)) {\n                    define_dir($path . "/" . $file, $functions);\n                } else {\n                    if (substr($file, - 4, 4) != ".php") continue;\n                    define_file($path . "/" . $file, $functions);\n                }\n            }\n        }       \n    }\n    function define_file($path, &amp;$functions) {\n        $tokens = token_get_all(file_get_contents($path));\n        for ($i = 0; $i &lt; count($tokens); $i++) {\n            $token = $tokens[$i];\n            if (is_array($token)) {\n                if ($token[0] != T_FUNCTION) continue;\n                $i++;\n                $token = $tokens[$i];\n                if ($token[0] != T_WHITESPACE) die("T_WHITESPACE");\n                $i++;\n                $token = $tokens[$i];\n                if ($token[0] != T_STRING) die("T_STRING");\n                $functions[$token[1]][0][] = array($path, $token[2]);\n            }\n        }\n    }\n    function reference_dir($path, &amp;$functions) {\n        if ($dir = opendir($path)) {\n            while (($file = readdir($dir)) !== false) {\n                if (substr($file, 0, 1) == ".") continue;\n                if (is_dir($path . "/" . $file)) {\n                    reference_dir($path . "/" . $file, $functions);\n                } else {\n                    if (substr($file, - 4, 4) != ".php") continue;\n                    reference_file($path . "/" . $file, $functions);\n                }\n            }\n        }       \n    }\n    function reference_file($path, &amp;$functions) {\n        $tokens = token_get_all(file_get_contents($path));\n        for ($i = 0; $i &lt; count($tokens); $i++) {\n            $token = $tokens[$i];\n            if (is_array($token)) {\n                if ($token[0] != T_STRING) continue;\n                if ($tokens[$i + 1] != "(") continue;\n                $functions[$token[1]][1][] = array($path, $token[2]);\n            }\n        }\n    }\n?&gt;\n</code></pre>\n\n<p>I\'ll probably spend some more time on it so I can quickly find the files and line numbers of the function definitions and references; this information is being gathered, just not displayed.</p>\n'}, {'answer_id': 693991, 'author': 'Till', 'author_id': 2859, 'author_profile': 'https://Stackoverflow.com/users/2859', 'pm_score': 2, 'selected': False, 'text': '<p>If I remember correctly you can use <a href="http://phpcallgraph.sourceforge.net/" rel="nofollow noreferrer">phpCallGraph</a> to do that. It\'ll generate a nice graph (image) for you with all the methods involved. If a method is not connected to any other, that\'s a good sign that the method is orphaned.</p>\n\n<p>Here\'s an example: <a href="http://phpcallgraph.sourceforge.net/examples/refactoring-candidates/class_GallerySystem.png" rel="nofollow noreferrer">classGallerySystem.png</a></p>\n\n<p>The method <code>getKeywordSetOfCategories()</code> is orphaned.</p>\n\n<p>Just by the way, you don\'t have to take an image -- phpCallGraph can also <em>generate</em> a text file, or a PHP array, etc..</p>\n'}, {'answer_id': 2946691, 'author': 'webbiedave', 'author_id': 294972, 'author_profile': 'https://Stackoverflow.com/users/294972', 'pm_score': 2, 'selected': False, 'text': '<p>Because PHP functions/methods can be dynamically invoked, there is no programmatic way to know with certainty if a function will never be called.</p>\n\n<p>The only certain way is through manual analysis.</p>\n'}, {'answer_id': 2948406, 'author': 'symcbean', 'author_id': 223992, 'author_profile': 'https://Stackoverflow.com/users/223992', 'pm_score': 0, 'selected': False, 'text': '<p><a href="http://phpxref.sourceforge.net/" rel="nofollow noreferrer">phpxref</a> will identify where functions are called from which would facilitate the analysis - but there\'s still a certain amount of manual effort involved.</p>\n'}, {'answer_id': 2953417, 'author': 'manuel aldana', 'author_id': 236370, 'author_profile': 'https://Stackoverflow.com/users/236370', 'pm_score': -1, 'selected': False, 'text': '<p>afaik there is no way. To know which functions "are belonging to whom" you would need to execute the system (runtime late binding function lookup).  </p>\n\n<p>But Refactoring tools are based on static code analysis. I really like dynamic typed languages, but in my view they are difficult to scale. The lack of safe refactorings in large codebases and dynamic typed languages is a major drawback for maintainability and handling software evolution.</p>\n'}, {'answer_id': 4791987, 'author': 'Gordon', 'author_id': 208809, 'author_profile': 'https://Stackoverflow.com/users/208809', 'pm_score': 5, 'selected': False, 'text': '<p>You can try Sebastian Bergmann\'s Dead Code Detector:</p>\n\n<blockquote>\n  <p><code>phpdcd</code> is a Dead Code Detector (DCD) for PHP code. It scans a PHP project for all declared functions and methods and reports those as being "dead code" that are not called at least once.</p>\n</blockquote>\n\n<p>Source: <a href="https://github.com/sebastianbergmann/phpdcd" rel="nofollow noreferrer">https://github.com/sebastianbergmann/phpdcd</a></p>\n\n<p>Note that it\'s a static code analyzer, so it might give false positives for methods that only called dynamically, e.g. it cannot detect <code>$foo = \'fn\'; $foo();</code></p>\n\n<p>You can install it via PEAR:</p>\n\n<pre><code>pear install phpunit/phpdcd-beta\n</code></pre>\n\n<p>After that you can use with the following options:</p>\n\n<pre><code>Usage: phpdcd [switches] &lt;directory|file&gt; ...\n\n--recursive Report code as dead if it is only called by dead code.\n\n--exclude &lt;dir&gt; Exclude &lt;dir&gt; from code analysis.\n--suffixes &lt;suffix&gt; A comma-separated list of file suffixes to check.\n\n--help Prints this usage information.\n--version Prints the version and exits.\n\n--verbose Print progress bar.\n</code></pre>\n\n<p>More tools:</p>\n\n<ul>\n<li><a href="https://phpqa.io/" rel="nofollow noreferrer">https://phpqa.io/</a></li>\n</ul>\n\n<hr>\n\n<p><strong>Note:</strong> as per the repository notice, <em>this project is no longer maintained and its repository is only kept for archival purposes</em>. So your mileage may vary.</p>\n'}, {'answer_id': 7133253, 'author': 'Andrey Butov', 'author_id': 137484, 'author_profile': 'https://Stackoverflow.com/users/137484', 'pm_score': 3, 'selected': False, 'text': '<p>USAGE: <strong>find_unused_functions.php &lt;root_directory&gt;</strong></p>\n<p>NOTE: This is a ‘quick-n-dirty’ approach to the problem. This script only performs a lexical pass over the files, and does not respect situations where different modules define identically named functions or methods. If you use an IDE for your PHP development, it may offer a more comprehensive solution.</p>\n<p>Requires PHP 5</p>\n<p>To save you a copy and paste, a direct download, and any new versions, are <strong><a href="https://github.com/andreybutov/find-unused-php-functions" rel="nofollow noreferrer">available here</a></strong>.</p>\n<pre><code>#!/usr/bin/php -f\n \n&lt;?php\n \n// ============================================================================\n//\n// find_unused_functions.php\n//\n// Find unused functions in a set of PHP files.\n// version 1.3\n//\n// ============================================================================\n//\n// Copyright (c) 2011, Andrey Butov. All Rights Reserved.\n// This script is provided as is, without warranty of any kind.\n//\n// http://www.andreybutov.com\n//\n// ============================================================================\n \n// This may take a bit of memory...\nini_set(\'memory_limit\', \'2048M\');\n \nif ( !isset($argv[1]) ) \n{\n    usage();\n}\n \n$root_dir = $argv[1];\n \nif ( !is_dir($root_dir) || !is_readable($root_dir) )\n{\n    echo &quot;ERROR: \'$root_dir\' is not a readable directory.\\n&quot;;\n    usage();\n}\n \n$files = php_files($root_dir);\n$tokenized = array();\n \nif ( count($files) == 0 )\n{\n    echo &quot;No PHP files found.\\n&quot;;\n    exit;\n}\n \n$defined_functions = array();\n \nforeach ( $files as $file )\n{\n    $tokens = tokenize($file);\n \n    if ( $tokens )\n    {\n        // We retain the tokenized versions of each file,\n        // because we\'ll be using the tokens later to search\n        // for function \'uses\', and we don\'t want to \n        // re-tokenize the same files again.\n \n        $tokenized[$file] = $tokens;\n \n        for ( $i = 0 ; $i &lt; count($tokens) ; ++$i )\n        {\n            $current_token = $tokens[$i];\n            $next_token = safe_arr($tokens, $i + 2, false);\n \n            if ( is_array($current_token) &amp;&amp; $next_token &amp;&amp; is_array($next_token) )\n            {\n                if ( safe_arr($current_token, 0) == T_FUNCTION )\n                {\n                    // Find the \'function\' token, then try to grab the \n                    // token that is the name of the function being defined.\n                    // \n                    // For every defined function, retain the file and line\n                    // location where that function is defined. Since different\n                    // modules can define a functions with the same name,\n                    // we retain multiple definition locations for each function name.\n \n                    $function_name = safe_arr($next_token, 1, false);\n                    $line = safe_arr($next_token, 2, false);\n \n                    if ( $function_name &amp;&amp; $line )\n                    {\n                        $function_name = trim($function_name);\n                        if ( $function_name != &quot;&quot; )\n                        {\n                            $defined_functions[$function_name][] = array(\'file\' =&gt; $file, \'line\' =&gt; $line);\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n \n// We now have a collection of defined functions and\n// their definition locations. Go through the tokens again, \n// and find \'uses\' of the function names. \n \nforeach ( $tokenized as $file =&gt; $tokens )\n{\n    foreach ( $tokens as $token )\n    {\n        if ( is_array($token) &amp;&amp; safe_arr($token, 0) == T_STRING )\n        {\n            $function_name = safe_arr($token, 1, false);\n            $function_line = safe_arr($token, 2, false);;\n \n            if ( $function_name &amp;&amp; $function_line )\n            {\n                $locations_of_defined_function = safe_arr($defined_functions, $function_name, false);\n \n                if ( $locations_of_defined_function )\n                {\n                    $found_function_definition = false;\n \n                    foreach ( $locations_of_defined_function as $location_of_defined_function )\n                    {\n                        $function_defined_in_file = $location_of_defined_function[\'file\'];\n                        $function_defined_on_line = $location_of_defined_function[\'line\'];\n \n                        if ( $function_defined_in_file == $file &amp;&amp; \n                             $function_defined_on_line == $function_line )\n                        {\n                            $found_function_definition = true;\n                            break;\n                        }\n                    }\n \n                    if ( !$found_function_definition )\n                    {\n                        // We found usage of the function name in a context\n                        // that is not the definition of that function. \n                        // Consider the function as \'used\'.\n \n                        unset($defined_functions[$function_name]);\n                    }\n                }\n            }\n        }\n    }\n}\n \n \nprint_report($defined_functions);   \nexit;\n \n \n// ============================================================================\n \nfunction php_files($path) \n{\n    // Get a listing of all the .php files contained within the $path\n    // directory and its subdirectories.\n \n    $matches = array();\n    $folders = array(rtrim($path, DIRECTORY_SEPARATOR));\n \n    while( $folder = array_shift($folders) ) \n    {\n        $matches = array_merge($matches, glob($folder.DIRECTORY_SEPARATOR.&quot;*.php&quot;, 0));\n        $moreFolders = glob($folder.DIRECTORY_SEPARATOR.\'*\', GLOB_ONLYDIR);\n        $folders = array_merge($folders, $moreFolders);\n    }\n \n    return $matches;\n}\n \n// ============================================================================\n \nfunction safe_arr($arr, $i, $default = &quot;&quot;)\n{\n    return isset($arr[$i]) ? $arr[$i] : $default;\n}\n \n// ============================================================================\n \nfunction tokenize($file)\n{\n    $file_contents = file_get_contents($file);\n \n    if ( !$file_contents )\n    {\n        return false;\n    }\n \n    $tokens = token_get_all($file_contents);\n    return ($tokens &amp;&amp; count($tokens) &gt; 0) ? $tokens : false;\n}\n \n// ============================================================================\n \nfunction usage()\n{\n    global $argv;\n    $file = (isset($argv[0])) ? basename($argv[0]) : &quot;find_unused_functions.php&quot;;\n    die(&quot;USAGE: $file &lt;root_directory&gt;\\n\\n&quot;);\n}\n \n// ============================================================================\n \nfunction print_report($unused_functions)\n{\n    if ( count($unused_functions) == 0 )\n    {\n        echo &quot;No unused functions found.\\n&quot;;\n    }\n \n    $count = 0;\n    foreach ( $unused_functions as $function =&gt; $locations )\n    {\n        foreach ( $locations as $location )\n        {\n            echo &quot;\'$function\' in {$location[\'file\']} on line {$location[\'line\']}\\n&quot;;\n            $count++;\n        }\n    }\n \n    echo &quot;=======================================\\n&quot;;\n    echo &quot;Found $count unused function&quot; . (($count == 1) ? \'\' : \'s\') . &quot;.\\n\\n&quot;;\n}\n \n// ============================================================================\n \n/* EOF */\n</code></pre>\n'}, {'answer_id': 9979425, 'author': 'Tim Cullen', 'author_id': 1088372, 'author_profile': 'https://Stackoverflow.com/users/1088372', 'pm_score': 4, 'selected': False, 'text': '<p>This bit of bash scripting might help:</p>\n\n<pre><code>grep -rhio ^function\\ .*\\(  .|awk -F\'[( ]\'  \'{print "echo -n " $2 " &amp;&amp; grep -rin " $2 " .|grep -v function|wc -l"}\'|bash|grep 0\n</code></pre>\n\n<p>This basically recursively greps the current directory for function definitions, passes the hits to awk, which forms a command to do the following:</p>\n\n<ul>\n<li>print the function name</li>\n<li>recursively grep for it again</li>\n<li>piping that output to grep -v to filter out function definitions so as to retain calls to the function</li>\n<li>pipes this output to wc -l which prints the line count</li>\n</ul>\n\n<p>This command is then sent for execution to bash and the output is grepped for 0, which would indicate 0 calls to the function.</p>\n\n<p>Note that this will <em>not</em> solve the problem calebbrown cites above, so there might be some false positives in the output.</p>\n'}, {'answer_id': 55196470, 'author': 'Tomas Votruba', 'author_id': 1348344, 'author_profile': 'https://Stackoverflow.com/users/1348344', 'pm_score': 2, 'selected': False, 'text': '<h2>2019+ Update</h2>\n\n<p>I got inspied by <a href="https://stackoverflow.com/questions/11532/how-can-i-find-unused-functions-in-a-php-project/7133253#7133253">Andrey\'s answer</a> and turned this into a coding standard sniff.</p>\n\n<p>The detection is very simple yet powerful:</p>\n\n<ul>\n<li>finds all methods <code>public function someMethod()</code></li>\n<li>then find all method calls <code>${anything}-&gt;someMethod()</code></li>\n<li>and simply <strong>reports those public functions that were never called</strong></li>\n</ul>\n\n<p>It helped me to remove <a href="https://github.com/Symplify/Symplify/commit/fa21855694d933716117b2a2db13acac55b86d69" rel="nofollow noreferrer">over</a> <a href="https://github.com/Symplify/Symplify/commit/3f08ed1fb2f22dd6c4a7b46d680adf4ab5a0907d" rel="nofollow noreferrer">20+ methods</a> I would have to maintain and test.</p>\n\n<p><br></p>\n\n<h2>3 Steps to Find them</h2>\n\n<p>Install ECS:</p>\n\n<pre><code>composer require symplify/easy-coding-standard --dev\n</code></pre>\n\n<p>Set up <code>ecs.yaml</code> config:</p>\n\n<pre><code># ecs.yaml\nservices:\n    Symplify\\CodingStandard\\Sniffs\\DeadCode\\UnusedPublicMethodSniff: ~\n</code></pre>\n\n<p>Run the command:</p>\n\n<pre><code>vendor/bin/ecs check src\n</code></pre>\n\n<p>See reported methods and remove those you don\'t fine useful  </p>\n\n<hr>\n\n<p>You can read more about it here: <a href="https://www.tomasvotruba.cz/blog/2019/03/14/remove-dead-public-methdos-from-your-code/" rel="nofollow noreferrer">Remove Dead Public Methods from Your Code</a></p>\n'}, {'answer_id': 64443364, 'author': 'Sandy Garrido', 'author_id': 7696704, 'author_profile': 'https://Stackoverflow.com/users/7696704', 'pm_score': 3, 'selected': False, 'text': '<h1>2020 Update</h1>\n<p>I have used the other methods outlined above, even the <strong>2019 update answer here is outdated</strong>.</p>\n<p>Tomáš Votruba\'s answer led me to find Phan as the ECS route has now been deprecated. Symplify have removed the dead public method checker.</p>\n<h2>Phan is a static analyzer for PHP</h2>\n<p>We can utilise Phan to search for dead code. Here are the steps to take using composer to install. These steps are also found on the <a href="https://github.com/phan/phan" rel="noreferrer">git repo for phan</a>. These instructions assume you\'re at the root of your project.</p>\n<h2>Step 1 - Install Phan w/ composer</h2>\n<pre><code>composer require phan/phan\n</code></pre>\n<h2>Step 2 - Install php-ast</h2>\n<p><code>PHP-AST</code> is a requirement for Phan\nAs I\'m using WSL, I\'ve been able to use PECL to install, however, other install methods for <code>php-ast</code> can be found in a git repo</p>\n<pre><code>pecl install ast\n</code></pre>\n<h2>Step 3 - Locate and edit php.ini to use php-ast</h2>\n<p>Locate current <code>php.ini</code></p>\n<pre><code>php -i | grep \'php.ini\'\n</code></pre>\n<p>Now take that file location and nano (or whichever of your choice to edit this doc). Locate the area of all extensions and ADD the following line:</p>\n<pre><code>extension=ast.so\n</code></pre>\n<h2>Step 4 - create a config file for Phan</h2>\n<p>Steps on config file can be found in Phan\'s documentation on <a href="https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file" rel="noreferrer">how to create a config file</a>\nYou\'ll want to use their sample one as it\'s a good starting point. Edit the following arrays to add your own paths on both\n<code>directory_list</code> &amp; <code>exclude_analysis_directory_list</code>.\nPlease note that <code>exclude_analysis_directory_list</code> will still be parsed but not validated eg. adding Wordpress directory here would mean, false positives for called wordpress functions in your theme would not appear as it found the function in wordpress but at the same time it\'ll not validate functions in wordpress\' folder.\nMine looked like this</p>\n<pre><code>......\n\n\'directory_list\' =&gt; [\n  \'public_html\'\n],\n\n......\n\n\'exclude_analysis_directory_list\' =&gt; [\n    \'vendor/\',\n    \'public_html/app/plugins\',\n    \'public_html/app/mu-plugins\',\n    \'public_html/admin\'\n],\n......\n</code></pre>\n<h2>Step 5 - Run Phan with dead code detection</h2>\n<p>Now that we\'ve installed phan and ast, configured the folders we wish to parse, it\'s time to run Phan. We\'ll be passing an argument to phan <code>--dead-code-detection</code> which is self explanatory.</p>\n<pre><code>./vendor/bin/phan --dead-code-detection\n</code></pre>\n<p>This output will need verifying with a fine tooth comb but it\'s certainly the best place to start</p>\n<p>The output will look like this in console</p>\n<pre><code>the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\\the\\path\\to\\function::the_funciton()\nthe/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\\the\\path\\to\\function::the_funciton()\nthe/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\\the\\path\\to\\function::the_funciton()\nthe/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\\the\\path\\to\\function::the_funciton()\n</code></pre>\n<p>Please feel free to add to this answer or correct my mistakes :)</p>\n'}]
['https://Stackoverflow.com/questions/11532', 'https://Stackoverflow.com', 'https://Stackoverflow.com/users/1142/']