I've searched all over for something like this but I believe the word "use" is perhaps too common for any helpful results:
What is the easiest way to remove all un-used use statements from class files in a PHP codebase?
Edit: For the sake of simplicity, we can ignore detecting use statements that are used for annotations.
Check FriendsOfPHP's PHP-CS-Fixer https://github.com/FriendsOfPHP/PHP-CS-Fixer
EDIT
I have completely rewritten it, so now it is much more powerful:
Traits and anonymous/lambda functions are ignored
Now taking care of catch blocks, class extensions and interfaces
Indentation and comments don't matter
Multiple declarations for namespace aliases work too
Static and object class calls are recognized as "usage" ($u->getUsages())
Full and half qualified usages are not treated
The test file, class.php:
<?php
use My\Full\Classname as Another, My\Full\NSname, Some\Other\Space;
/* some insane commentary */ use My\Full\NSname1; use ArrayObject;
$obj = new namespaced\Another;
$obj = new Another;
$a = new ArrayObject(array(1));
Space::call();
$a = function($a, $b, $c = 'test') use ($obj) {
/* use */
};
class MyHelloWorld extends Base {
use traits, hello, world;
}
And here the script:
<?php
class UseStatementSanitzier
{
protected $content;
public function __construct($file)
{
$this->content = token_get_all(file_get_contents($file));
// we don't need and want them while parsing
$this->removeTokens(T_COMMENT);
$this->removeTokens(T_WHITESPACE);
}
public function getUnused()
{
$uses = $this->getUseStatements();
$usages = $this->getUsages();
$unused = array();
foreach($uses as $use) {
if (!in_array($use, $usages)) {
$unused[] = $use;
}
}
return $unused;
}
public function getUsages()
{
$usages = array();
foreach($this->content as $key => $token) {
if (!is_string($token)) {
$t = $this->content;
// for static calls
if ($token[0] == T_DOUBLE_COLON) {
// only if it is NOT full or half qualified namespace
if ($t[$key-2][0] != T_NAMESPACE) {
$usages[] = $t[$key-1][1];
}
}
// for object instanciations
if ($token[0] == T_NEW) {
if ($t[$key+2][0] != T_NAMESPACE) {
$usages[] = $t[$key+1][1];
}
}
// for class extensions
if ($token[0] == T_EXTENDS || $token[0] == T_IMPLEMENTS) {
if ($t[$key+2][0] != T_NAMESPACE) {
$usages[] = $t[$key+1][1];
}
}
// for catch blocks
if ($token[0] == T_CATCH) {
if ($t[$key+3][0] != T_NAMESPACE) {
$usages[] = $t[$key+2][1];
}
}
}
}
return array_values(array_unique($usages));
}
public function getUseStatements()
{
$tokenUses = array();
$level = 0;
foreach($this->content as $key => $token) {
// for traits, only first level uses should be captured
if (is_string($token)) {
if ($token == '{') {
$level++;
}
if ($token == '}') {
$level--;
}
}
// capture all use statements besides trait-uses in class
if (!is_string($token) && $token[0] == T_USE && $level == 0) {
$tokenUses[] = $key;
}
}
$useStatements = array();
// get rid of uses in lambda functions
foreach($tokenUses as $key => $tokenKey) {
$i = $tokenKey;
$char = '';
$useStatements[$key] = '';
while($char != ';') {
++$i;
$char = is_string($this->content[$i]) ? $this->content[$i] : $this->content[$i][1];
if (!is_string($this->content[$i]) && $this->content[$i][0] == T_AS) {
$useStatements[$key] .= ' AS ';
} else {
$useStatements[$key] .= $char;
}
if ($char == '(') {
unset($useStatements[$key]);
break;
}
}
}
$allUses = array();
// get all use statements
foreach($useStatements as $fullStmt) {
$fullStmt = rtrim($fullStmt, ';');
$fullStmt = preg_replace('/^.+ AS /', '', $fullStmt);
$fullStmt = explode(',', $fullStmt);
foreach($fullStmt as $singleStmt) {
// $singleStmt only for full qualified use
$fqUses[] = $singleStmt;
$singleStmt = explode('\\', $singleStmt);
$allUses[] = array_pop($singleStmt);
}
}
return $allUses;
}
public function removeTokens($tokenId)
{
foreach($this->content as $key => $token) {
if (isset($token[0]) && $token[0] == $tokenId) {
unset($this->content[$key]);
}
}
// reindex
$this->content = array_values($this->content);
}
}
$unused = new UseStatementSanitzier('class.php');
print_r($unused->getUnused());
/*
Returns:
Array
(
[0] => NSname
[1] => NSname1
)
*/
It would probably depend on the way your code is set up. If your code uses namespaces like so:
namespace Foo
{
<one or more classes in namespace Foo>
}
then you're probably fine if you just check each file individually. That still means you would have to parse the PHP code to find the use statements, and then to determine which statements are used.
The easy way is to use a tool that's already been built. I recently started using PhpStorm IDE (30 day free trail, or the early access program) and that can inform you when you have unused use statements in a file (and you can even specify whether that should come up as warnings or errors). It would still require you to open each file though. But you could also check files you are editing, then eventually your code will be cleaner.
Related
For testing/debugging/evaluation purposes I want to write wrappers for selected entries of the PHP autoloader chain. As autoloader methods are restricted to a single argument with a given value (the class name) the only way to tell the wrapper what to do seems to be the method name. I thus iterate over spl_autoload_functions(), replace the selected items with dynamically created calls of methods like Wrapper::handler_xxxx and save the original information in that class. Wrapper has a method public static function __callStatic() which should evaluate the incoming requests, do its testing/debugging/evaluating work and otherwise redirect to the (saved) original handler. However, this does not work but leads to errors like this one:
PHP Fatal error: Uncaught LogicException: Passed array does not specify an existing method (class 'Wrapper' does not have a method 'autoload_mdGVzdG1ldGhvZAo') in [somewhere]
Seems that the autoloader needs real methods and does not work with PHP-magic. Whether this is a bug or a feature (if the latter: why?), is there any way to work around it?
Edit: some code example upon request, I am not sure if this helps understanding the problem; this is by far not finished, but just an attempt to get the basic idea up and running. Calling the magic method manually (by the name given above) works fine - it is just that the autoloader-logic is not willing to call it.
class Wrapper {
const AUTOLOAD_PREFIX = 'autoload';
public static function __callStatic(String $name, Array $arguments) {
if (strpos($name, self::AUTOLOAD_PREFIX) === 0) {
try {
# valid signature found
if (preg_match(sprintf('/^%s(_c([^_]+))?_m([^_]+)$/', self::AUTOLOAD_PREFIX), $name, $matches)) {
# call via class/method
$method = self::decodeMethodName($matches[3]);
if ($matches[2]) {
$class = self::decodeMethodName($matches[2]);
$class::$method($arguments);
}
else {
# call global function
$method($arguments);
}
}
else {
# invalid name
throw new \Exception(sprintf('Invalid static call of %s::%s', __CLASS__, $name));
}
}
catch (\Throwable $e) {
echo "I got you.";
die;
}
}
else {
throw new \Exception(sprintf('Invalid static call of %s::%s', __CLASS__, $name));
}
return;
}
private static function encodeMethodName(String $method) : String {
# protect and encode method names
return str_replace(array('+', '/', '='), array(0x81, 0x82, ''), base64_encode($method));
}
private static function decodeMethodName(String $method) : String {
# reconstruct method names
return base64_decode(str_replace(array(0x80, 0x81), array('+', '/'), $method));
}
public function protectAutoloader(?String $originalClass, String $originalMethod) : Void {
$stack = array();
$autoLoaders = spl_autoload_functions();
while (!$done && count($autoLoaders) > 0) {
$item = array_pop($autoLoaders);
if (
is_string($item) &&
is_null($originalClass) &&
($originalMethod === $item)
) {
# global function
$replacement = array(
__CLASS__,
sprintf('%s_m%s', self::AUTOLOAD_PREFIX, self::encodeMethodName($item)),
);
$done = true;
}
elseif (
is_array($item) &&
($item[0] === $originalClass) &&
($item[1] === $originalMethod)
) {
# static method
$replacement = array(
__CLASS__,
sprintf('%s_c%s_m%s', self::AUTOLOAD_PREFIX, self::encodeMethodName($item[0]), self::encodeMethodName($item[1])),
);
$done = true;
}
else {
# don't touch anything else (closures)
$replacement = $item;
}
# remove item and push to the stack
spl_autoload_unregister($item);
array_push($stack, $replacement);
}
# restore autoloader chain
while (count($stack) > 0) {
$item = array_pop($stack);
spl_autoload_register($item, true);
}
return;
}
I'd activate this for a specific autoloader:
$wrapper->protectAutoloader(NULL, 'testmethod');
Now testmethod() (if existing in the chain) will be replaced by Wrapper::autoload_mdGVzdG1ldGhvZAo() and if e.g. a syntactically broken file were autoloaded the message "I got you" would be printed.
I have one PHP class as below (part of the code):
class myclass{
private static $arrX = array();
private function is_val_exists($needle, $haystack) {
if(in_array($needle, $haystack)) {
return true;
}
foreach($haystack as $element) {
if(is_array($element) && $this->is_val_exists($needle, $element))
return true;
}
return false;
}
//the $anInput is a string e.g. Michael,18
public function doProcess($anInput){
$det = explode(",", $anInput);
if( $this->is_val_exists( $det[0], $this->returnProcess() ) ){
//update age of Michael
}
else{
array_push(self::$arrX, array(
'name' => $det[0],
'age' => $det[1]
));
}
}
public function returnProcess(){
return self::$arrX;
}
}
The calling code in index.php
$msg = 'Michael,18';
myclass::getHandle()->doProcess($msg);
In my webpage says index.php, it calls function doProcess() over and over again. When the function is called, string is passed and stored in an array. In the next call, if let's say same name is passed again, I want to update his age. My problem is I don't know how to check if the array $arrX contains the name. From my own finding, the array seems to be re-initiated (back to zero element) when the code is called. My code never does the update and always go to the array_push part. Hope somebody can give some thoughts on this. Thank you.
There is a ) missing in your else condition of your doProcess() function, it should read:
else{
array_push(self::$arrX, array(
'name' => $det[0],
'age' => $det[1]
)); // <-- there was the missing )
}
Here is a complete running solution based on your code:
<?php
class myclass{
private static $arrX = array();
private function is_val_exists($needle, $haystack) {
if(in_array($needle, $haystack)) {
return true;
}
foreach($haystack as $element) {
if(is_array($element) && $this->is_val_exists($needle, $element))
return true;
}
return false;
}
//the $anInput is a string e.g. Michael,18
public function doProcess($anInput){
$det = explode(",", $anInput);
if( $this->is_val_exists( $det[0], $this->returnProcess() ) ){
//update age of Michael
for ($i=0; $i<count(self::$arrX); $i++) {
if (is_array(self::$arrX[$i]) && self::$arrX[$i]['name'] == $det[0]) {
self::$arrX[$i]['age'] = $det[1];
break;
}
}
} else{
array_push(self::$arrX, array(
'name' => $det[0],
'age' => $det[1]
));
}
}
public function returnProcess(){
return self::$arrX;
}
}
$mc = new myclass();
$mc->doProcess('Michael,18');
$mc->doProcess('John,23');
$mc->doProcess('Michael,19');
$mc->doProcess('John,25');
print_r($mc->returnProcess());
?>
You can test it here: PHP Runnable
As I said in comments, it looks like you want to maintain state between requests. You can't use pure PHP to do that, you should use an external storage solution instead. If it's available, try Redis, it has what you need and is quite simple to use. Or, if you're familiar with SQL, you could go with MySQL for example.
On a side note, you should read more about how PHP arrays work.
Instead of array_push, you could have just used self::$arrX[] = ...
Instead of that, you could have used an associative array, e.g. self::$arrX[$det[0]] = $det[1];, that would make lookup much easier (array_key_exists etc.)
Can you try updating the is_val_exists as follows:
private function is_val_exists($needle, $haystack) {
foreach($haystack as $element) {
if ($element['name'] == $needle) {
return true;
}
return false;
}
I am have written a helper function to "cleanup" callback variables for input into MySQL. This is the function that I wrote:
public function string($object, $objectPath) {
if (!empty($object->$objectPath) || $object->$objectPath !== '') {
$value = $object->$objectPath;
} else {
return 'NULL';
}
if (!empty($value) || $value != '') {
return "'".str_replace("'","''",$value)."'";
} else {
return 'NULL';
}
}
Now, $object is always an object returned by the call, and $objectPath is always a string to points to a given value. Here's where the problem comes in. This works:
$value = $this->db->string($object, 'foo');
However, this does not work:
$value = $this->db->string($object, 'foo->bar->foo1->bar1');
Whenever $objectPath is more than "one layer" deep, I get the following error from (Amazon's) client library:
Fatal error: Call to undefined method MarketplaceWebServiceOrders_Model_Order::getFoo->Bar() in /path/to/Model.php on line 63
The code block that the error refers to is this:
public function __get($propertyName)
{
$getter = "get$propertyName";
return $this->$getter(); // this is line 63
}
$object is not XML, so I can't use SimpleXMLElement and XPath.
What is the problem with my code? Is it that am I concatenating an object and a string? If so, how can I make that possible? How can I get this function to do what I intended it to do?
By the way, I'm using PHP 5.4.27.
PHP doesn't automatically resolve a string containing multiple path levels to children of an object like you are attempting to do.
This will not work even if $obj contains the child hierarchy you are expecting:
$obj = ...;
$path = 'level1->level2->level3';
echo $obj->$path; // WRONG!
You would need to split up the path and "walk" through the object trying to resolve the final property.
Here is an example based on yours:
<?php
$obj = new stdClass();
$obj->name = 'Fred';
$obj->job = new stdClass();
$obj->job->position = 'Janitor';
$obj->job->years = 4;
print_r($obj);
echo 'Years in current job: '.string($obj, 'job->years').PHP_EOL;
function string($obj, $path_str)
{
$val = null;
$path = preg_split('/->/', $path_str);
$node = $obj;
while (($prop = array_shift($path)) !== null) {
if (!is_object($obj) || !property_exists($node, $prop)) {
$val = null;
break;
}
$val = $node->$prop;
// TODO: Insert any logic here for cleaning up $val
$node = $node->$prop;
}
return $val;
}
Here it is working: http://3v4l.org/9L4gc
With #itsmejodie's help, I finally got a working solution:
public function string($node, $objectPath) {
$value = NULL;
$path = explode('->', $objectPath);
while (($prop = array_shift($path)) !== NULL) {
if (!$node->$prop) {
break;
}
$value = $node->$prop;
$node = $node->$prop;
}
if (is_string($value)) {
return "'".str_replace("'","''",$value)."'";
} else {
return 'NULL';
}
}
The key for me was to see that, as #itsmejodie put it, "PHP doesn't automatically resolve a string containing multiple path levels to children of an object." In a string like, 'foo->bar->foo1->bar2', PHP won't convert the ->'s into the T_OBJECT_OPERATOR, thus appending a string to an object, e.g., $object->foo->bar->foo1->bar2, just won't work.
PROBLEM
I have a function that takes in a nested array where the structure and nesting of the array is unknown at run-time. All that is known is some of the target fieldnames and desired values of some of the leafs.
QUESTIONS
1) I am hoping to modify this unknown structure and still have the code be readable and easily understood by fellow programmers. What (if any) solution will allow me to do things like this in PHP?
// Pseudo-code for things I would like to be able to do
// this is kinda like the same thing as XPATH, but for native PHP array
// find *every* fname with value of "Brad" and change it to "Brian"
$mydata->find_all('*:fname')->where_value_eq('Brad')->set_equal_to('Brian');
// find *the first* fave_color and set it to "Green"
$mydata->find('*:fave_color')->get(0)->set_equal_to('Green');
2) If there is nothing out there that will let me do this, is there something, anything, that at least comes close to the spirit of what I am hoping to accomplish here?
SAMPLE ARRAY
$mydata = array(
'people' => array(
array('fname'=>'Alice'),
array('fname'=>'Brad'),
array('fname'=>'Chris'),
),
'animals' => array(
array('fname'=>'Dino'),
array('fname'=>'Lassie'),
array('fname'=>'Brad'),
),
'settings' => array(
'user_prefs'=>array(
'localhost'=>array(
'fave_color'=>'blue',
),
),
),
'places' => array(
array('state'=>'New york',
'cities'=>array(
'name'=>'Albany',
'name'=>'Buffalo',
'name'=>'Corning',
),
'state'=>'California',
'cities'=>array(
'name'=>'Anaheim',
'name'=>'Bakersfield',
'name'=>'Carlsbad',
),
),
),
);
Although I maintain that you should stick with explicit manipulation as in my previous answer, boredom and intrigue got the better of me ;)
It probably has holes (and clearly lacks docs) but if you insist on this route, it should get you started:
class Finder {
protected $data;
public function __construct(&$data) {
if (!is_array($data)) {
throw new InvalidArgumentException;
}
$this->data = &$data;
}
public function all() {
return $this->find();
}
public function find($expression = null) {
if (!isset($expression)) {
return new Results($this->data);
}
$results = array();
$this->_find(explode(':', $expression), $this->data, $results);
return new Results($results);
}
protected function _find($parts, &$data, &$results) {
if (!$parts) {
return;
}
$currentParts = $parts;
$search = array_shift($currentParts);
if ($wildcard = $search == '*') {
$search = array_shift($currentParts);
}
foreach ($data as $key => &$value) {
if ($key === $search) {
if ($currentParts) {
$this->_find($currentParts, $value, $results);
} else {
$results[] = &$value;
}
} else if ($wildcard && is_array($value)) {
$this->_find($parts, $value, $results);
}
}
}
}
class Results {
protected $data;
public function __construct(&$data) {
$this->data = $data;
}
public function get($index, $limit = 1) {
$this->data = array_slice($this->data, $index, $limit);
return $this;
}
public function set_equal_to($value) {
foreach ($this->data as &$datum) {
$datum = $value;
}
}
public function __call($method, $args) {
if (!preg_match('/^where_?(key|value)_?(eq|contains)$/i', $method, $m)) {
throw new BadFunctionCallException;
}
if (!isset($args[0])) {
throw new InvalidArgumentException;
}
$operand = $args[0];
$isKey = strtolower($m[1]) == 'key';
$method = array('Results', '_compare' . (strtolower($m[2]) == 'eq' ? 'EqualTo' : 'Contains'));
$ret = array();
foreach ($this->data as $key => &$datum) {
if (call_user_func($method, $isKey ? $key : $datum, $operand)) {
$ret[] = &$datum;
}
}
$this->data = $ret;
return $this;
}
protected function _compareEqualTo($value, $test) {
return $value == $test;
}
protected function _compareContains($value, $test) {
return strpos($value, $test) !== false;
}
}
$finder = new Finder($mydata);
$finder->find('*:fname')->where_value_eq('Brad')->set_equal_to('Brian');
$finder->find('*:fave_color')->get(0)->set_equal_to('Green');
$finder->find('places:*:cities:*:name')->where_value_contains('ba')->set_equal_to('Stackoton');
print_r($mydata);
There's certainly no native solution for this and the syntax is rather strange. If you want the code to "be readable and easily understood by fellow programmers" please stick to methods that we're used to working with ;)
foreach ($mydata as $type => &$data) {
foreach ($data as &$member) {
if (isset($member['fname']) && $member['fname'] == 'Brad') {
$member['fname'] = 'Brian';
}
}
}
It's admittedly more verbose, but there's much less chance of confusion.
I have a single xml parsing function that I'm trying to call multiple times as I only need to strip a little data out and continue on.
Here is the function:
//Parse Product ID from Product Sides
function getProductSpecs($xml,$type) {
// Setup arrary
global $productspecs;
global $count;
$count = 0;
global $type_check;
$type_check = $type;
// Parse the XML
// Create the parser
if (! ($xmlparser = xml_parser_create()) )
{
die ("Cannot create name list parser");
}
// Start tag function
function first($parser, $name, $attribs) {
global $trigger;
if ($name == "PRODUCTSIDEID") {
$trigger = 1;
} elseif ($name == "PRODUCTID") {
$trigger = 1;
}
}
// data handler function
function xml($parser, $data) {
global $trigger;
global $productspecs;
global $count;
global $type_check;
if ($trigger == 1){
if ($type_check == "sideid") {
$productspecs[$count]=$data;
$count = $count + 1;
} elseif ($type_check == "productid") {
$productspecs[$count]=$data;
$count = $count + 1;
}
$trigger = 0;
}
}
// Call the handler functions
xml_set_element_handler($xmlparser, "first", "");
// Call the data handler
xml_set_character_data_handler($xmlparser, "xml");
// Parse the XML data
xml_parse($xmlparser,$xml);
// Clear parser
xml_parser_free($xmlparser);
//Return the array
return $productspecs;
}
My problem arises when this is called:
xml_set_element_handler($xmlparser, "first", "");
I get the redeclare error on:
function first($parser, $name, $attribs) {
The function only appears the one time and I'm assuming the problem occurs on the call but is there a way around this so I don't have to duplicate so much code. I'm going to have to iterate through this multiple times.
Thanks.
Defining functions inside of functions can lead to this. Each time you run getProductSpecs() it's going to try to declare first() and xml() again, and in PHP, all user functions are declared in a global scope. The best solution is to move your first() function and your xml() function outside of the main getProductSpecs() function.
Another option is to use function_exists() around your function declarations, like this:
if (! function_exists('first')) {
// Start tag function
function first($parser, $name, $attribs) {
global $trigger;
if ($name == "PRODUCTSIDEID") {
$trigger = 1;
} elseif ($name == "PRODUCTID") {
$trigger = 1;
}
}
}