phpunit merge two or more clover.xml reports - php

I have several clover.xml reports of different extensions of a projects. I want to combine them into one clover.xml and then create it into a clover html. But i see no way with the phpunit classes PHP_CodeCoverage, PHP_CodeCoverage_Report_HTML, PHP_CodeCoverage_Report_Clover.
None of these classes accept an existing clover.xml. I thought I might be able to work with the methods append and merge of PHP_CodeCoverage. But that does not accept files.

If you are running jenkins or similar include a php script in your Ant build file to merge the files using SimpleXML
An example is here
http://kuttler.eu/post/merging-and-splitting-xml-files-with-simplexml/
Then in your post build actions jenkins will use the clover.xml to generate your code coverage

As jkrnak commented above you cannot simply merge the XML files as there are computed values such as lines covered etc.. that are computed at output time. You need to "merge" while still working with native PHP code. In my case I wanted to capture the coverage of a series of web service calls executed by newman. To do this I set a flag at the beginning of execution which persists across invocations (using a cache) and then also save the PHP_CodeCoverage object in the cache as well. My implementation (in Laravel) looks something like this:
if ( isset($_GET['initCoverage']) )
{
Cache::put( 'recordCoverage', true, 1440 );
}
if ( Cache::has('recordCoverage') )
{
if ( Cache::has('coverage') )
{
$coverage = Cache::get('coverage');
}
else
{
$filter = new PHP_CodeCoverage_Filter;
$filter->addDirectoryToBlacklist( base_path() . '/vendor' );
$coverage = new PHP_CodeCoverage( null, $filter );
}
$coverage->start( Request::method() . " " . Request::path() );
if ( isset($_GET['dumpCoverage']) )
{
if ( Cache::has('coverage') )
{
// Prevent timeout as writing coverage reports takes a long time
set_time_limit( 0 );
$coverage = Cache::get( 'coverage' );
$writer = new PHP_CodeCoverage_Report_Clover;
$writer->process($coverage, 'results/coverage/clover.xml');
}
Cache::forget('recordCoverage');
Cache::forget('coverage');
}
else
{
register_shutdown_function( function($coverage)
{
$coverage->stop();
Cache::put( 'coverage', $coverage, 1440);
}, $coverage);
}
}
This captures the series of tests in a single coverage object which is then output when I make a call with the "dumpCoverage" flag.

Years later this issue is still partly unsolved. There is a project by SB that can merge clover files, but it requires php 5.6.
None of the answers above work sufficiently well. Here is a gist of a merge thrown together. Constructive critisism welcome.
Usage:
php clover-merge.php -o merged.xml -f clover-phpunit.xml -f clover-phpspec.xml
Posting it here for posterity too:
<?php
$options = getopt("f:o:");
if (! isset($options['f'])) {
echo "Files have to be specified with -f\n";
exit(1);
}
if (! isset($options['o'])) {
echo "Output has to be specified with -o\n";
exit(1);
}
$files = $options['f'];
if (! is_array($files)) {
$files = array($files);
}
$output = $options['o'];
$buffer = '';
foreach ($files as $file) {
if (! file_exists($file)) {
echo "File '$file' doesn't exist\n";
exit(2);
}
$report = simplexml_load_file($file);
$buffer .= $report->project->asXML();
}
$fh = fopen($output ,'w');
if (! $fh) {
echo "Cannot open '$output' for writing\n";
exit(2);
}
fwrite($fh, sprintf('<?xml version="1.0" encoding="UTF-8"?><coverage>%s</coverage>', $buffer));
fclose($fh);

Related

PHP Phar creation slow on a powerful PC, how to speed up (loading/reading ~3000 files)?

I'm trying to pack my web application (Symfony 2 project) with Phar. I've successfully packed Silex, a micro framework with hundred of files in a reasonable time (1-2 minutes).
The problem is on my development machine (i7 4770k, 16GB, SSD Raid 0, project on a RAM disk) creating the archive is really slow, it takes ~1 sec for each file. I really need to find out a way to speed up things.
Single iteration of reading/loading the file is slow. I'm adding files using:
function addFile(Phar $phar, SplFileInfo $file)
{
$root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
$path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
$phar->addFromString($path, file_get_contents($file));
}
$phar = new Phar(/* ... */);
$phar->startBuffering();
// ...
foreach ($files as $file) {
addFile($phar, $file);
}
// ...
$phar->setStub(/* ... */);
$phar->stopBuffering();
How can I speed up reading/adding files? Could be my OS (Windows) the problem?
EDIT: disabling buffering didn't solve the problem. Same speed adding from strings:
// This is VERY fast (~ 1 sec to read all 3000+ files)
$strings = array();
foreach ($files as $file) {
$root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
$path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
$strings[$path] = file_get_contents($file->getRealPath());
}
// This is SLOW
foreach ($strings as $local => $content) {
$phar->addFromString($local, $content);
}
EDIT: full quick&dirty script (may help) app/build:
#!/usr/bin/env php
<?php
set_time_limit(0);
require __DIR__.'/../vendor/autoload.php';
use Symfony\Component\Finder\Finder;
use Symfony\Component\Console\Input\ArgvInput;
$input = new ArgvInput();
$env = $input->getParameterOption(array('--env', '-e'), 'dev');
function addFile(Phar $phar, SplFileInfo $file)
{
$root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
$path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
$phar->addFromString($path, file_get_contents($file));
}
$phar = new Phar(__DIR__ . "/../symfony.phar", 0, "symfony.phar");
$phar->startBuffering();
$envexclude = array_diff(array('dev', 'prod', 'test'), array($env));
// App
$app = (new Finder())
->files()
->notPath('/cache/')
->notPath('/logs/')
->notName('build')
->notname('/._('.implode('|', $envexclude).')\.yml$/')
->in(__DIR__);
// Vendor
$vendor = (new Finder())
->files()
->ignoreVCS(true)
->name('*.{php,twig,xlf,xsd,xml}')
->notPath('/tests/i')
->notPath('/docs/i')
->in(__DIR__.'/../vendor');
// Src
$src = (new Finder())
->files()
->notPath('/tests/i')
->in(__DIR__.'/../src');
// Web
$web = (new Finder())
->files()
->in(__DIR__.'/../web')
->notname('/._('.implode('|', $envexclude).')\.php$/');
$all = array_merge(
iterator_to_array($app),
iterator_to_array($src),
iterator_to_array($vendor),
iterator_to_array($web)
);
$c = count($all);
$i = 1;
$strings = array();
foreach ($all as $file) {
addFile($phar, $file);
echo "Done $i/$c\r\n";
$i++;
}
$stub = <<<'STUB'
Phar::webPhar(null, "/web/app_phar.php", null, array(), function ($path) {
return '/web/app_phar.php'.$path;
});
__HALT_COMPILER();
STUB;
$phar->setStub($stub);
$phar->stopBuffering();
Try using Phar::addFile($file) instead of Phar::addFromString(file_get_contents($file))
i.e.
function addFile(Phar $phar, SplFileInfo $file)
{
$root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
$path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
//$phar->addFromString($path, file_get_contents($file));
$phar->addFile($file,$path);
}
Phar::addFromString() or Phar:addFromFile() is incredibly slow.
Like #sectus said Phar::buildFromDirectory() is a lot faster. But as an easy alternative with little changes to your code you could use Phar::buildFromIterator().
Example:
$all = $app->append($vendor)->append($src)->append($web);
$phar->buildFromIterator($all, dirname(__DIR__));
instead of:
$all = array_merge(
iterator_to_array($app),
iterator_to_array($src),
iterator_to_array($vendor),
iterator_to_array($web)
);
$c = count($all);
$i = 1;
$strings = array();
foreach ($all as $file) {
addFile($phar, $file);
echo "Done $i/$c\r\n";
$i++;
}
$ time app/build
real 0m4.459s
user 0m2.895s
sys 0m1.108s
Takes < 5 seconds on my quite slow ubuntu machine.
I'd suggest to dig in php config.
First recomendation - is to disable open_basedir if it is enabled. As i understand php internals, when u try to access any file location with php, it is a must for php to check if file location matches allowed directory-tree. So if there are many files this operation will be preformed for each file, and it can slow the proccess down significantly. On the other hand if open_basedir is disabled, check is never done.
http://www.php.net/manual/en/ini.core.php#ini.open-basedir
Second - is to check realpath_cache_size and realpath_cache_ttl.
As written in php description
Determines the size of the realpath cache to be used by PHP. This value should be increased on systems where PHP opens many files, to reflect the quantity of the file operations performed.
http://www.php.net/manual/en/ini.core.php#ini.realpath-cache-size
I hope this will help u to speed up ur operations.
You know, I have realized that Phar::buildFromDirectory is pretty fast.
$phar->buildFromDirectory('./src/', '/\.php$/');
But you need write more complicated regex. But you could call buildFromDirectory several times with different arguments.
Or create temporary folder and copy all files into from $all. Something like this
function myCopy($src, $dest)
{
#mkdir(dirname($dest), 0755, true);
copy($src, $dest);
}
foreach ($all as $file)
{
//$phar->addFile($file);
myCopy($file, './tmp/' . $file);
}
$phar->buildFromDirectory('./tmp/');
I would suggest using Preloader to concatenate all your files into a single file and then simply add that single file to the phar.
I know you said that adding the filename from string did not improve performance, but perhaps a different way of loading the file names can improve performance along with using the filename from string. Composer is pretty fast, but I never timed it. Try loading the files by group of filetypes and adding them as groups separately.
It uses a class from Symfony which you may not want or would change.
use Symfony\Component\Finder\Finder;
$phar = new \Phar(/* ... */);
$phar->setSignatureAlgorithm(\Phar::SHA1);
$phar->startBuffering();
$finder = new Finder();
//add php files with filter
$finder->files()
->ignoreVCS(true)
->name('*.php')
->notName('Compiler.php')
->notName('ClassLoader.php')
->in(__DIR__.'/..')
;
foreach ($finder as $file) {
$this->addFile($phar, $file);
}
$this->addFile($phar, new \SplFileInfo(/* ... */), false);
$finder = new Finder();
$finder->files()
->name('*.json')
->in(__DIR__ . '/../../res')
;
foreach ($finder as $file) {
$this->addFile($phar, $file, false);
}
$this->addFile($phar, new \SplFileInfo(/* ... */), false);
$phar->setStub($this->getStub());
$phar->stopBuffering();
Perhaps you can exclude a cache or log file using the Finer's filters if somehow its one large file that causes the long lag. Look at the composer link for full details on how its implemented.
what about using class instead of passing phar into func? just a piece of code to understand.. or ever heard about memory limit of php.ini or other setting that can slow down things.
class XY {
private $phar;
function addFile(SplFileInfo $file)
$root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
$path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
$this->phar->addFromString($path, file_get_contents($file));
}
// other code here
}
Correct me if i am wrong but in this way instead of passing the phar into function, you will avoid "copying" of object. this way is like pointer.
you can create threads and reduce total time, of course Symfony has to support concurrent loading. it is not actually best answer for your question, but it can significantly decrease total load time.
class Loader extends Thread {
public $phar;
public $file;
public $done;
public function run() {
$self->done = false;
addFile($self->phar, $self->file);
$self->done = true;
}
}
$threads = array();
foreach ($files as $file) {
$t = new Loader();
$t->phar = $phar;
$t->file = $file;
addFile($phar, $file);
$t->start();
$threads[] = $t;
}
while(true){
$finished = true;
foreach ($t as $threads) {
if ($t->done == false){
$finished = false;
sleep(1);
break;
}
}
if ($finished)
break;
}
and, creating 3000 threads is not good idea. you might need to create well thread-worker logic.
Try to disable GC like composer did
gc_disable();

PHP Auto Delete an Array of Unwanted Files From All Folders on Server Script

I have a couple of PHP scripts for deleting error_log, .DS_Store etc files from all folders on my entire server. I simply have these scripts uploaded to my root (public_html) and visit them periodically when I want to do a little cleanup. When I visit the URL of where the scripts are loaded it automatically gets to work. That's all perfect and how I'd like to continue using it.
However, I'd love to consolidate this automation into just one script where I can list an array of the undesirable files like so:
$unwanted_filenames = array(
'.DS_Store',
'.localized',
'Thumbs.db',
'error_log'
);
And simply run through all folders and delete all the files of which I've listed in the array.
The scripts I use now are overkill, listing out every individual file and how much it's freed up etc. I'm a minimalist and would love the simplest script with the least amount of code to just get the job done.
So when I visit the page it automatically get's to work, a white screen of nothing is fine and then maybe a simple "Done. Freed up 3MB." message. That's it.
OK - here's the shortest but of PHP I can think of that'll do it:
$unwanted_filenames = array(
'.DS_Store',
'.localized',
'Thumbs.db',
'error_log'
);
$it = new RecursiveDirectoryIterator("/"); // Set starting directory here
foreach(new RecursiveIteratorIterator($it) as $file) {
if (in_array(basename($file), $unwanted_filenames)) {
#unlink($file); // THe # hides errors, remove if you want to see them
}
}
Hopefully self-explanatory - and yes, it does subdirectories (that's the "recursive" bit).
And you said minamalistic, so I didn't include the freed space, but just add a $FreedSpace += filesize($file) before the unlink if you want to add that in.
I'm using this, you can do it like this:
<?php
$dir = "/var/www/vhosts/"; //Write your dirname here
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
$total_thumbs = 0;
$total_ds = 0;
foreach ($rii as $file) {
if ($file->isDir()){
continue;
}
$parcala = explode('.', $file->getFilename());
$uzanti = end($parcala);
if ($file->getFilename() == 'Thumbs.db') {
unlink($file->getPathname());
$total_thumbs++;
}
if ($uzanti == '.DS_Store' || $file->getFilename() == 'DS_Store' || $file->getFilename() == '.DS_Store') {
unlink($file->getPathname());
$total_ds++;
}
}
echo $total_thumbs . ' Thumbs.db file and ' . $total_ds . ' DS_Store file deleted!';
And if you want automation you can use Cronjob

Is there a PHP refactoring tool to tell what functions are never called? [duplicate]

How can I find any unused functions in a PHP project?
Are there features or APIs built into PHP that will allow me to analyse my codebase - for example Reflection, token_get_all()?
Are these APIs feature rich enough for me not to have to rely on a third party tool to perform this type of analysis?
You can try Sebastian Bergmann's Dead Code Detector:
phpdcd 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.
Source: https://github.com/sebastianbergmann/phpdcd
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 $foo = 'fn'; $foo();
You can install it via PEAR:
pear install phpunit/phpdcd-beta
After that you can use with the following options:
Usage: phpdcd [switches] <directory|file> ...
--recursive Report code as dead if it is only called by dead code.
--exclude <dir> Exclude <dir> from code analysis.
--suffixes <suffix> A comma-separated list of file suffixes to check.
--help Prints this usage information.
--version Prints the version and exits.
--verbose Print progress bar.
More tools:
https://phpqa.io/
Note: as per the repository notice, this project is no longer maintained and its repository is only kept for archival purposes. So your mileage may vary.
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:
<?php
$functions = array();
$path = "/path/to/my/php/project";
define_dir($path, $functions);
reference_dir($path, $functions);
echo
"<table>" .
"<tr>" .
"<th>Name</th>" .
"<th>Defined</th>" .
"<th>Referenced</th>" .
"</tr>";
foreach ($functions as $name => $value) {
echo
"<tr>" .
"<td>" . htmlentities($name) . "</td>" .
"<td>" . (isset($value[0]) ? count($value[0]) : "-") . "</td>" .
"<td>" . (isset($value[1]) ? count($value[1]) : "-") . "</td>" .
"</tr>";
}
echo "</table>";
function define_dir($path, &$functions) {
if ($dir = opendir($path)) {
while (($file = readdir($dir)) !== false) {
if (substr($file, 0, 1) == ".") continue;
if (is_dir($path . "/" . $file)) {
define_dir($path . "/" . $file, $functions);
} else {
if (substr($file, - 4, 4) != ".php") continue;
define_file($path . "/" . $file, $functions);
}
}
}
}
function define_file($path, &$functions) {
$tokens = token_get_all(file_get_contents($path));
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (is_array($token)) {
if ($token[0] != T_FUNCTION) continue;
$i++;
$token = $tokens[$i];
if ($token[0] != T_WHITESPACE) die("T_WHITESPACE");
$i++;
$token = $tokens[$i];
if ($token[0] != T_STRING) die("T_STRING");
$functions[$token[1]][0][] = array($path, $token[2]);
}
}
}
function reference_dir($path, &$functions) {
if ($dir = opendir($path)) {
while (($file = readdir($dir)) !== false) {
if (substr($file, 0, 1) == ".") continue;
if (is_dir($path . "/" . $file)) {
reference_dir($path . "/" . $file, $functions);
} else {
if (substr($file, - 4, 4) != ".php") continue;
reference_file($path . "/" . $file, $functions);
}
}
}
}
function reference_file($path, &$functions) {
$tokens = token_get_all(file_get_contents($path));
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (is_array($token)) {
if ($token[0] != T_STRING) continue;
if ($tokens[$i + 1] != "(") continue;
$functions[$token[1]][1][] = array($path, $token[2]);
}
}
}
?>
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.
This bit of bash scripting might help:
grep -rhio ^function\ .*\( .|awk -F'[( ]' '{print "echo -n " $2 " && grep -rin " $2 " .|grep -v function|wc -l"}'|bash|grep 0
This basically recursively greps the current directory for function definitions, passes the hits to awk, which forms a command to do the following:
print the function name
recursively grep for it again
piping that output to grep -v to filter out function definitions so as to retain calls to the function
pipes this output to wc -l which prints the line count
This command is then sent for execution to bash and the output is grepped for 0, which would indicate 0 calls to the function.
Note that this will not solve the problem calebbrown cites above, so there might be some false positives in the output.
USAGE: find_unused_functions.php <root_directory>
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.
Requires PHP 5
To save you a copy and paste, a direct download, and any new versions, are available here.
#!/usr/bin/php -f
<?php
// ============================================================================
//
// find_unused_functions.php
//
// Find unused functions in a set of PHP files.
// version 1.3
//
// ============================================================================
//
// Copyright (c) 2011, Andrey Butov. All Rights Reserved.
// This script is provided as is, without warranty of any kind.
//
// http://www.andreybutov.com
//
// ============================================================================
// This may take a bit of memory...
ini_set('memory_limit', '2048M');
if ( !isset($argv[1]) )
{
usage();
}
$root_dir = $argv[1];
if ( !is_dir($root_dir) || !is_readable($root_dir) )
{
echo "ERROR: '$root_dir' is not a readable directory.\n";
usage();
}
$files = php_files($root_dir);
$tokenized = array();
if ( count($files) == 0 )
{
echo "No PHP files found.\n";
exit;
}
$defined_functions = array();
foreach ( $files as $file )
{
$tokens = tokenize($file);
if ( $tokens )
{
// We retain the tokenized versions of each file,
// because we'll be using the tokens later to search
// for function 'uses', and we don't want to
// re-tokenize the same files again.
$tokenized[$file] = $tokens;
for ( $i = 0 ; $i < count($tokens) ; ++$i )
{
$current_token = $tokens[$i];
$next_token = safe_arr($tokens, $i + 2, false);
if ( is_array($current_token) && $next_token && is_array($next_token) )
{
if ( safe_arr($current_token, 0) == T_FUNCTION )
{
// Find the 'function' token, then try to grab the
// token that is the name of the function being defined.
//
// For every defined function, retain the file and line
// location where that function is defined. Since different
// modules can define a functions with the same name,
// we retain multiple definition locations for each function name.
$function_name = safe_arr($next_token, 1, false);
$line = safe_arr($next_token, 2, false);
if ( $function_name && $line )
{
$function_name = trim($function_name);
if ( $function_name != "" )
{
$defined_functions[$function_name][] = array('file' => $file, 'line' => $line);
}
}
}
}
}
}
}
// We now have a collection of defined functions and
// their definition locations. Go through the tokens again,
// and find 'uses' of the function names.
foreach ( $tokenized as $file => $tokens )
{
foreach ( $tokens as $token )
{
if ( is_array($token) && safe_arr($token, 0) == T_STRING )
{
$function_name = safe_arr($token, 1, false);
$function_line = safe_arr($token, 2, false);;
if ( $function_name && $function_line )
{
$locations_of_defined_function = safe_arr($defined_functions, $function_name, false);
if ( $locations_of_defined_function )
{
$found_function_definition = false;
foreach ( $locations_of_defined_function as $location_of_defined_function )
{
$function_defined_in_file = $location_of_defined_function['file'];
$function_defined_on_line = $location_of_defined_function['line'];
if ( $function_defined_in_file == $file &&
$function_defined_on_line == $function_line )
{
$found_function_definition = true;
break;
}
}
if ( !$found_function_definition )
{
// We found usage of the function name in a context
// that is not the definition of that function.
// Consider the function as 'used'.
unset($defined_functions[$function_name]);
}
}
}
}
}
}
print_report($defined_functions);
exit;
// ============================================================================
function php_files($path)
{
// Get a listing of all the .php files contained within the $path
// directory and its subdirectories.
$matches = array();
$folders = array(rtrim($path, DIRECTORY_SEPARATOR));
while( $folder = array_shift($folders) )
{
$matches = array_merge($matches, glob($folder.DIRECTORY_SEPARATOR."*.php", 0));
$moreFolders = glob($folder.DIRECTORY_SEPARATOR.'*', GLOB_ONLYDIR);
$folders = array_merge($folders, $moreFolders);
}
return $matches;
}
// ============================================================================
function safe_arr($arr, $i, $default = "")
{
return isset($arr[$i]) ? $arr[$i] : $default;
}
// ============================================================================
function tokenize($file)
{
$file_contents = file_get_contents($file);
if ( !$file_contents )
{
return false;
}
$tokens = token_get_all($file_contents);
return ($tokens && count($tokens) > 0) ? $tokens : false;
}
// ============================================================================
function usage()
{
global $argv;
$file = (isset($argv[0])) ? basename($argv[0]) : "find_unused_functions.php";
die("USAGE: $file <root_directory>\n\n");
}
// ============================================================================
function print_report($unused_functions)
{
if ( count($unused_functions) == 0 )
{
echo "No unused functions found.\n";
}
$count = 0;
foreach ( $unused_functions as $function => $locations )
{
foreach ( $locations as $location )
{
echo "'$function' in {$location['file']} on line {$location['line']}\n";
$count++;
}
}
echo "=======================================\n";
echo "Found $count unused function" . (($count == 1) ? '' : 's') . ".\n\n";
}
// ============================================================================
/* EOF */
2020 Update
I have used the other methods outlined above, even the 2019 update answer here is outdated.
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.
Phan is a static analyzer for PHP
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 git repo for phan. These instructions assume you're at the root of your project.
Step 1 - Install Phan w/ composer
composer require phan/phan
Step 2 - Install php-ast
PHP-AST is a requirement for Phan
As I'm using WSL, I've been able to use PECL to install, however, other install methods for php-ast can be found in a git repo
pecl install ast
Step 3 - Locate and edit php.ini to use php-ast
Locate current php.ini
php -i | grep 'php.ini'
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:
extension=ast.so
Step 4 - create a config file for Phan
Steps on config file can be found in Phan's documentation on how to create a config file
You'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
directory_list & exclude_analysis_directory_list.
Please note that exclude_analysis_directory_list 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.
Mine looked like this
......
'directory_list' => [
'public_html'
],
......
'exclude_analysis_directory_list' => [
'vendor/',
'public_html/app/plugins',
'public_html/app/mu-plugins',
'public_html/admin'
],
......
Step 5 - Run Phan with dead code detection
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 --dead-code-detection which is self explanatory.
./vendor/bin/phan --dead-code-detection
This output will need verifying with a fine tooth comb but it's certainly the best place to start
The output will look like this in console
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
Please feel free to add to this answer or correct my mistakes :)
If I remember correctly you can use phpCallGraph 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.
Here's an example: classGallerySystem.png
The method getKeywordSetOfCategories() is orphaned.
Just by the way, you don't have to take an image -- phpCallGraph can also generate a text file, or a PHP array, etc..
Because PHP functions/methods can be dynamically invoked, there is no programmatic way to know with certainty if a function will never be called.
The only certain way is through manual analysis.
2019+ Update
I got inspied by Andrey's answer and turned this into a coding standard sniff.
The detection is very simple yet powerful:
finds all methods public function someMethod()
then find all method calls ${anything}->someMethod()
and simply reports those public functions that were never called
It helped me to remove over 20+ methods I would have to maintain and test.
3 Steps to Find them
Install ECS:
composer require symplify/easy-coding-standard --dev
Set up ecs.yaml config:
# ecs.yaml
services:
Symplify\CodingStandard\Sniffs\DeadCode\UnusedPublicMethodSniff: ~
Run the command:
vendor/bin/ecs check src
See reported methods and remove those you don't fine useful 👍
You can read more about it here: Remove Dead Public Methods from Your Code
phpxref will identify where functions are called from which would facilitate the analysis - but there's still a certain amount of manual effort involved.
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).
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.

PHP Check Process ID

This is something i have wondered for a while and decided to ask about it.
We have the function getmypid() which will return the current scripts process id. Is there some kind of function such as
checkifpidexists() in php? I mean a inbuilt one and not some batch script solution.
And is there a way to change a scripts pid?
Some clarification:
I want to check if a pid exists to see if the script is already running so it dont run again, faux cron job if you will.
The reason i wanted to change the pid is so i can set the script pid to something really high such as 60000 and hard code that value so this script can only run on that pid so only 1 instance of it would run
EDIT----
To help anyone else with this proplem, i have created this class:
class instance {
private $lock_file = '';
private $is_running = false;
public function __construct($id = __FILE__) {
$id = md5($id);
$this->lock_file = sys_get_temp_dir() . $id;
if (file_exists($this->lock_file)) {
$this->is_running = true;
} else {
$file = fopen($this->lock_file, 'w');
fclose($file);
}
}
public function __destruct() {
if (file_exists($this->lock_file) && !$this->is_running) {
unlink($this->lock_file);
}
}
public function is_running() {
return $this->is_running;
}
}
and you use it like so:
$instance = new instance('abcd'); // the argument is optional as it defaults to __FILE__
if ($instance->is_running()) {
echo 'file already running';
} else {
echo 'file not running';
}
In linux, you would look at /proc.
return file_exists( "/proc/$pid" );
In Windows you could shell_exec() tasklist.exe, and that would find a matching process id:
$processes = explode( "\n", shell_exec( "tasklist.exe" ));
foreach( $processes as $process )
{
if( strpos( "Image Name", $process ) === 0
|| strpos( "===", $process ) === 0 )
continue;
$matches = false;
preg_match( "/(.*?)\s+(\d+).*$/", $process, $matches );
$pid = $matches[ 2 ];
}
I believe what you want to do is maintain a PID file. In the daemons I've written, they check a config file, look for an instance of a pid file, get the pid out of the pid file, check to see if /proc/$pid exists, and if not, delete the pid file.
if( file_exists("/tmp/daemon.pid"))
{
$pid = file_get_contents( "/tmp/daemon.pid" );
if( file_exists( "/proc/$pid" ))
{
error_log( "found a running instance, exiting.");
exit(1);
}
else
{
error_log( "previous process exited without cleaning pidfile, removing" );
unlink( "/tmp/daemon.pid" );
}
}
$h = fopen("/tmp/daemon.pid", 'w');
if( $h ) fwrite( $h, getmypid() );
fclose( $h );
Process IDs are granted by the OS and one cannot reserve a process id. You would write your daemon to respect the pid file.
A better way to accomplish this would be to use a pid or a lock file. Simply check for the existence of the pid file, create it as necessary, and populate it with your running pid.
<?
class pidfile {
private $_file;
private $_running;
public function __construct($dir, $name) {
$this->_file = "$dir/$name.pid";
if (file_exists($this->_file)) {
$pid = trim(file_get_contents($this->_file));
if (posix_kill($pid, 0)) {
$this->_running = true;
}
}
if (! $this->_running) {
$pid = getmypid();
file_put_contents($this->_file, $pid);
}
}
public function __destruct() {
if ((! $this->_running) && file_exists($this->_file)) {
unlink($this->_file);
}
}
public function is_already_running() {
return $this->_running;
}
}
?>
And use it as follows:
<?
$pidfile = new pidfile('/tmp', 'myscript');
if($pidfile->is_already_running()) {
echo "Already running.\n";
exit;
} else {
echo "Started...\n";
}
?>
There's not much error checking here, but a quick run shows this works on my system.
For checking if a PID exist on a windows machine i use:
function pidExists($pid)
{
exec('TASKLIST /NH /FO "CSV" /FI "PID eq '.$pid.'"', $outputA );
$outputB = explode( '","', $outputA[0] );
return isset($outputB[1])?true:false;
}
Note that $outputB[0] contains a messages that pid can't be found, if the pid indeed doesn't exists! So to validate i use the second argument.
EDIT:
To expand on this, its also possible to dynamically spawn scripts within windows in the background using powershell like so:
// this function builds an argument list to parse into the newly spawned script.
// which can be accessed through the superglobal global $argv;
function buildArgList( array $arguments){
return ' '. implode(' ', $arguments) .' ';
}
$arguments = buildArgList(['argument1','argument2','argument3']);
$windowstyle = 'normal'; // or you can use hidden to hide the CLI
pclose(popen("powershell start-process -FilePath '" . PHP_BINARY . "' -ArgumentList '-f\"" . $file . " " . $arguments . "\"' -WindowStyle " . $windowstyle,"r"));
The script you spawn can then use: cli_set_process_title
to set that process's title to some unique hash.
within the parent that spawned the child process you can use the following code to find that process within the tasklist using its windowtitle searching for the uniquehash.
exec('TASKLIST /NH /FO "CSV" /FI "windowtitle eq ' . escapeshellarg($uniquehash) . '"', $output );
When combined with a database you can essentially build a workermanager
communicating between different php scripts.
No you cannot change any processes pid. It is assigned by the kernel and is part of the kernel's data structures
As others have said, you cannot change the process id - it is assigned and entirely manged by the kernel of the OS. Additionally, you have not said if this is command-line or web-server based: if it's the latter you may not even be getting the pid of your script.
The manual page for getmypid() contains some examples of "optimistic" locking. I use the word optimisitc as PHP is never ever going to approach the likes of an asp.net web application where you have a true threaded environment with shared/static classes and thus Singleton's to use/abuse. Basically you have the option of:
Touching a "lock file" on the file-system somewhere. Your script then checks if that file exists: if it does, terminate, otherwise, touch that file and carry on processing
Setting a database based flag to say the script is running. As above, but use a db table/field to mark a script as running.
Both of these rely on the script terminating correctly (as the last step would be to remove the lock file/db flag). If a script crashes for any reason (or the machine itself), you can be left with a manual tidy-up process to remove the flag. There is no easy solution for this, but one avenue to explore would be to then look at date-stamping the lock, with an arbitary "if older than X, the last run must have crashed" approach.
Don't forget also that you can access shell commands via backticks (`), which would give you access to the standard *nix tools for working with pids.
source: http://www.php.net/manual/en/language.operators.execution.php

How can I find unused functions in a PHP project

How can I find any unused functions in a PHP project?
Are there features or APIs built into PHP that will allow me to analyse my codebase - for example Reflection, token_get_all()?
Are these APIs feature rich enough for me not to have to rely on a third party tool to perform this type of analysis?
You can try Sebastian Bergmann's Dead Code Detector:
phpdcd 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.
Source: https://github.com/sebastianbergmann/phpdcd
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 $foo = 'fn'; $foo();
You can install it via PEAR:
pear install phpunit/phpdcd-beta
After that you can use with the following options:
Usage: phpdcd [switches] <directory|file> ...
--recursive Report code as dead if it is only called by dead code.
--exclude <dir> Exclude <dir> from code analysis.
--suffixes <suffix> A comma-separated list of file suffixes to check.
--help Prints this usage information.
--version Prints the version and exits.
--verbose Print progress bar.
More tools:
https://phpqa.io/
Note: as per the repository notice, this project is no longer maintained and its repository is only kept for archival purposes. So your mileage may vary.
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:
<?php
$functions = array();
$path = "/path/to/my/php/project";
define_dir($path, $functions);
reference_dir($path, $functions);
echo
"<table>" .
"<tr>" .
"<th>Name</th>" .
"<th>Defined</th>" .
"<th>Referenced</th>" .
"</tr>";
foreach ($functions as $name => $value) {
echo
"<tr>" .
"<td>" . htmlentities($name) . "</td>" .
"<td>" . (isset($value[0]) ? count($value[0]) : "-") . "</td>" .
"<td>" . (isset($value[1]) ? count($value[1]) : "-") . "</td>" .
"</tr>";
}
echo "</table>";
function define_dir($path, &$functions) {
if ($dir = opendir($path)) {
while (($file = readdir($dir)) !== false) {
if (substr($file, 0, 1) == ".") continue;
if (is_dir($path . "/" . $file)) {
define_dir($path . "/" . $file, $functions);
} else {
if (substr($file, - 4, 4) != ".php") continue;
define_file($path . "/" . $file, $functions);
}
}
}
}
function define_file($path, &$functions) {
$tokens = token_get_all(file_get_contents($path));
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (is_array($token)) {
if ($token[0] != T_FUNCTION) continue;
$i++;
$token = $tokens[$i];
if ($token[0] != T_WHITESPACE) die("T_WHITESPACE");
$i++;
$token = $tokens[$i];
if ($token[0] != T_STRING) die("T_STRING");
$functions[$token[1]][0][] = array($path, $token[2]);
}
}
}
function reference_dir($path, &$functions) {
if ($dir = opendir($path)) {
while (($file = readdir($dir)) !== false) {
if (substr($file, 0, 1) == ".") continue;
if (is_dir($path . "/" . $file)) {
reference_dir($path . "/" . $file, $functions);
} else {
if (substr($file, - 4, 4) != ".php") continue;
reference_file($path . "/" . $file, $functions);
}
}
}
}
function reference_file($path, &$functions) {
$tokens = token_get_all(file_get_contents($path));
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (is_array($token)) {
if ($token[0] != T_STRING) continue;
if ($tokens[$i + 1] != "(") continue;
$functions[$token[1]][1][] = array($path, $token[2]);
}
}
}
?>
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.
This bit of bash scripting might help:
grep -rhio ^function\ .*\( .|awk -F'[( ]' '{print "echo -n " $2 " && grep -rin " $2 " .|grep -v function|wc -l"}'|bash|grep 0
This basically recursively greps the current directory for function definitions, passes the hits to awk, which forms a command to do the following:
print the function name
recursively grep for it again
piping that output to grep -v to filter out function definitions so as to retain calls to the function
pipes this output to wc -l which prints the line count
This command is then sent for execution to bash and the output is grepped for 0, which would indicate 0 calls to the function.
Note that this will not solve the problem calebbrown cites above, so there might be some false positives in the output.
USAGE: find_unused_functions.php <root_directory>
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.
Requires PHP 5
To save you a copy and paste, a direct download, and any new versions, are available here.
#!/usr/bin/php -f
<?php
// ============================================================================
//
// find_unused_functions.php
//
// Find unused functions in a set of PHP files.
// version 1.3
//
// ============================================================================
//
// Copyright (c) 2011, Andrey Butov. All Rights Reserved.
// This script is provided as is, without warranty of any kind.
//
// http://www.andreybutov.com
//
// ============================================================================
// This may take a bit of memory...
ini_set('memory_limit', '2048M');
if ( !isset($argv[1]) )
{
usage();
}
$root_dir = $argv[1];
if ( !is_dir($root_dir) || !is_readable($root_dir) )
{
echo "ERROR: '$root_dir' is not a readable directory.\n";
usage();
}
$files = php_files($root_dir);
$tokenized = array();
if ( count($files) == 0 )
{
echo "No PHP files found.\n";
exit;
}
$defined_functions = array();
foreach ( $files as $file )
{
$tokens = tokenize($file);
if ( $tokens )
{
// We retain the tokenized versions of each file,
// because we'll be using the tokens later to search
// for function 'uses', and we don't want to
// re-tokenize the same files again.
$tokenized[$file] = $tokens;
for ( $i = 0 ; $i < count($tokens) ; ++$i )
{
$current_token = $tokens[$i];
$next_token = safe_arr($tokens, $i + 2, false);
if ( is_array($current_token) && $next_token && is_array($next_token) )
{
if ( safe_arr($current_token, 0) == T_FUNCTION )
{
// Find the 'function' token, then try to grab the
// token that is the name of the function being defined.
//
// For every defined function, retain the file and line
// location where that function is defined. Since different
// modules can define a functions with the same name,
// we retain multiple definition locations for each function name.
$function_name = safe_arr($next_token, 1, false);
$line = safe_arr($next_token, 2, false);
if ( $function_name && $line )
{
$function_name = trim($function_name);
if ( $function_name != "" )
{
$defined_functions[$function_name][] = array('file' => $file, 'line' => $line);
}
}
}
}
}
}
}
// We now have a collection of defined functions and
// their definition locations. Go through the tokens again,
// and find 'uses' of the function names.
foreach ( $tokenized as $file => $tokens )
{
foreach ( $tokens as $token )
{
if ( is_array($token) && safe_arr($token, 0) == T_STRING )
{
$function_name = safe_arr($token, 1, false);
$function_line = safe_arr($token, 2, false);;
if ( $function_name && $function_line )
{
$locations_of_defined_function = safe_arr($defined_functions, $function_name, false);
if ( $locations_of_defined_function )
{
$found_function_definition = false;
foreach ( $locations_of_defined_function as $location_of_defined_function )
{
$function_defined_in_file = $location_of_defined_function['file'];
$function_defined_on_line = $location_of_defined_function['line'];
if ( $function_defined_in_file == $file &&
$function_defined_on_line == $function_line )
{
$found_function_definition = true;
break;
}
}
if ( !$found_function_definition )
{
// We found usage of the function name in a context
// that is not the definition of that function.
// Consider the function as 'used'.
unset($defined_functions[$function_name]);
}
}
}
}
}
}
print_report($defined_functions);
exit;
// ============================================================================
function php_files($path)
{
// Get a listing of all the .php files contained within the $path
// directory and its subdirectories.
$matches = array();
$folders = array(rtrim($path, DIRECTORY_SEPARATOR));
while( $folder = array_shift($folders) )
{
$matches = array_merge($matches, glob($folder.DIRECTORY_SEPARATOR."*.php", 0));
$moreFolders = glob($folder.DIRECTORY_SEPARATOR.'*', GLOB_ONLYDIR);
$folders = array_merge($folders, $moreFolders);
}
return $matches;
}
// ============================================================================
function safe_arr($arr, $i, $default = "")
{
return isset($arr[$i]) ? $arr[$i] : $default;
}
// ============================================================================
function tokenize($file)
{
$file_contents = file_get_contents($file);
if ( !$file_contents )
{
return false;
}
$tokens = token_get_all($file_contents);
return ($tokens && count($tokens) > 0) ? $tokens : false;
}
// ============================================================================
function usage()
{
global $argv;
$file = (isset($argv[0])) ? basename($argv[0]) : "find_unused_functions.php";
die("USAGE: $file <root_directory>\n\n");
}
// ============================================================================
function print_report($unused_functions)
{
if ( count($unused_functions) == 0 )
{
echo "No unused functions found.\n";
}
$count = 0;
foreach ( $unused_functions as $function => $locations )
{
foreach ( $locations as $location )
{
echo "'$function' in {$location['file']} on line {$location['line']}\n";
$count++;
}
}
echo "=======================================\n";
echo "Found $count unused function" . (($count == 1) ? '' : 's') . ".\n\n";
}
// ============================================================================
/* EOF */
2020 Update
I have used the other methods outlined above, even the 2019 update answer here is outdated.
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.
Phan is a static analyzer for PHP
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 git repo for phan. These instructions assume you're at the root of your project.
Step 1 - Install Phan w/ composer
composer require phan/phan
Step 2 - Install php-ast
PHP-AST is a requirement for Phan
As I'm using WSL, I've been able to use PECL to install, however, other install methods for php-ast can be found in a git repo
pecl install ast
Step 3 - Locate and edit php.ini to use php-ast
Locate current php.ini
php -i | grep 'php.ini'
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:
extension=ast.so
Step 4 - create a config file for Phan
Steps on config file can be found in Phan's documentation on how to create a config file
You'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
directory_list & exclude_analysis_directory_list.
Please note that exclude_analysis_directory_list 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.
Mine looked like this
......
'directory_list' => [
'public_html'
],
......
'exclude_analysis_directory_list' => [
'vendor/',
'public_html/app/plugins',
'public_html/app/mu-plugins',
'public_html/admin'
],
......
Step 5 - Run Phan with dead code detection
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 --dead-code-detection which is self explanatory.
./vendor/bin/phan --dead-code-detection
This output will need verifying with a fine tooth comb but it's certainly the best place to start
The output will look like this in console
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
Please feel free to add to this answer or correct my mistakes :)
If I remember correctly you can use phpCallGraph 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.
Here's an example: classGallerySystem.png
The method getKeywordSetOfCategories() is orphaned.
Just by the way, you don't have to take an image -- phpCallGraph can also generate a text file, or a PHP array, etc..
Because PHP functions/methods can be dynamically invoked, there is no programmatic way to know with certainty if a function will never be called.
The only certain way is through manual analysis.
2019+ Update
I got inspied by Andrey's answer and turned this into a coding standard sniff.
The detection is very simple yet powerful:
finds all methods public function someMethod()
then find all method calls ${anything}->someMethod()
and simply reports those public functions that were never called
It helped me to remove over 20+ methods I would have to maintain and test.
3 Steps to Find them
Install ECS:
composer require symplify/easy-coding-standard --dev
Set up ecs.yaml config:
# ecs.yaml
services:
Symplify\CodingStandard\Sniffs\DeadCode\UnusedPublicMethodSniff: ~
Run the command:
vendor/bin/ecs check src
See reported methods and remove those you don't fine useful 👍
You can read more about it here: Remove Dead Public Methods from Your Code
phpxref will identify where functions are called from which would facilitate the analysis - but there's still a certain amount of manual effort involved.
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).
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.

Categories