Following this thread: How to handle functions deprecation in library? I want to find a way to track all calls to the deprecated function so I can make sure I get them all replaced before the function is removed. Given the following PHP methods
/*
#deprecated - just use getBar()
*/
function getFoo(){
return getBar();
}
function getBar(){
return "bar";
}
I came up with the following method of doing so and I'm looking for feedback.
function getFoo(){
try{
throw new Exception("Deprecated function used");
} catch(Exception $e){
//Log the Exception with stack trace
....
// return value as normal
return getBar();
}
}
For PHPs internal deprecated functions, just add E_STRICT to error_reporting.
For userland functions to raise Notice or Warning about deprecated functions, I'd suggest the developer who took the time to add the #deprecated annotation also triggers an E_USER_DEPRECATED warning, e.g.
function getFoo(){
trigger_error(__FUNCTION__ . 'is deprecated', E_USER_DEPRECATED );
return getBar();
}
I am not aware if any of the available QA tools can automatically detect if code contains deprecated method calls. Those are your best bet though.
You shouldn't need to be concerned about removing deprecated methods or functions if you are using TDD with 100% code coverage. Your automated tests will simply fail and you will know where to look.
Relying on the deprecated function being actually called is dangerous - you would have 100% code coverage to make sure you don't miss anything. It's all right for slowly finding all calls to deprecated functions and replace them one by one, but not good enough for a complete transition.
I think File > Search in Files
in your IDE is your best bet, as there are no good refactoring tools around for PHP that I know of.
Afterthought: Maybe PhpXRef is the solution.
Related
Since PHP7 it seems to be possible to directly call anonymous functions like this:
(function () { echo 'Hello!'; })();
I found this by accident in some open source code base. I tried to find any hint on this in the PHP documentation (including changelogs and RFCs). There seems to be none, it seems fully undocumented.
Is it safe to call functions this way or should I use call_user_func(); like in the old days? Is this documented somewhere?
I know of these questions and answers:
IIFE (Immediately Invoked Function Expression) in PHP?
https://stackoverflow.com/a/35044603/1708223
They just say it's supposed to work and how it works. I know this. My question is not about the "how"! It's about whether this is actually an official PHP feature or if it'S merely working "by accident" (because it seems undocumented).
The reason you won't find this documented as a feature in the main manual is because it isn't one, it's just a consequence of other features:
function () { echo 'Hello!'; } creates a Closure object
putting () after a Closure object executes that closure
That's not quite the same as accidental, but I can see why you might think that, because it didn't work in PHP 5.
The reason it changed is that PHP's parser was overhauled in PHP 7.0 to use more consistent rules when parsing variables, as well as introducing an extra stage called an Abstract Syntax Tree. This changed the meaning of some code, but allowed for other cases that logically should have been possible, but were previously hard to implement.
Allowing code that was previously a syntax error is not generally considered a compatibility break, so not every piece of syntax enabled by this change made it into migration guides.
However, removing this functionality - even if not documented - would be a compatibility break, so at the very least you will get some notice that it's going to change, and it would be in a "major version" (9.0, 10.0, etc). Since it's a useful piece of syntax, there's no reason to suppose it will ever stop working, unless there's some even more useful feature that conflicts with it for some reason.
Just to put some thoughts together: (function () { echo 'Hello!'; }) defines a class instance of type Closure. Putting the closure into a variable helps to understand that:
$x = (function () { echo 'Hello!'; });
echo gettype($x);
This prints object, since PHP 5.3. The result of $x(); hasn't changed since PHP 5.3 either. But what has changed is the unified handling of variables - this enables that you don't have to define a variable explicitly, but call the closure directly as if you had put it in a variable first
I am sitting on a large codebase that contains several classes that expose functionality through magically implemented methods (using __call and __callStatic). For example:
class Foo {
public function __call($name, $parameters) {
echo "You called $name().\n";
}
}
$f = new Foo;
$f->test(); // runs fine, but PhpStorm flags as a warning
The problem is that PhpStorm thinks that the test() method does not exist, so it gives a warning at the call site. This is a little annoying, as of course the code will run and behave as expected.
I have already tuned down the severity by checking the "downgrade severity if __magic methods are present in class" option, but I would prefer to either:
completely disable this functionality for specific classes only, or
work with the IDE rather than against it -- provide it with the information I already have so our views agree
Is any of the above possible? If so, how?
Additional bonus question: consider the case where method calls are being chained.
$f = new Foo;
$f->test()->chain()->moreChain(); // potentially runs fine
Assuming that the magic call to $f->test() returns something appropriate the subsequent (possibly, but not necessarily, also magic) calls will work fine. However, since there is no way that I know of to tell the IDE what test() returns it flags the rest of the call chain as full of missing methods too. And to make matters worse, the "downgrade severity" setting does not apply to these warnings since the IDE does not know what class these intermediate objects are supposed to be.
Is there a solution that can also cover this case?
Update
Even though documenting the magic methods with #method annotations seems to work, I have to assume that there are currently several problems with this approach because it only took me a little work to come upon these related bugs:
Type hinting for the method arguments does not work correctly with primitives
Annotations work for one call, but not for chained calls
I do hope they fix them in a reasonable time frame.
Well, you can go to the preference menu, under Inspections, go to Undefined -> Undefined Method and check Downgrade severity if __magic methods are present.
That would make the flag less severe, (instead of Warning, as Info), which would still give you a green light on your document check.
There's nothing else I'm aware of aside from having #property or #method PHPDoc notations on the target class for every method that's likely to be used.
Rather than globally turning off inspections by downgrading the severity of the inspection, you can add a comment to the single line to ignore just that particular inspection.
/** #noinspection PhpUndefinedMethodInspection */
Assertion::NullOrAssertionDoesNotExist();
Building on what Madara said, I found it wouldn't downgrade down far enough for my liking no matter what severity I set it to, so I made a new Severity for undefined method that has no attributes whatsoever, and turned off the downgrade checkbox (see image below)
If I hover over my magic methods now, it still brings up the popup message, but otherwise it doesn't distract me. One step better than just turning off inspection, because at least this way an actual undefined method can still be detected by hovering over the name
You can use a dynamic variable :
$test = 'test';
$chaine = $f->$test(); // no phpStorm flag, & the code works
What are possibilities to implement helper that will raise error log with level E_DEPRECATED (E_USER_DEPRECATED in fact) when class method with annotation #deprecated is called?
For example for the code
/**
* #deprecated
*/
public function main()
{}
when calling the method $obj->main() the deprecated warning would be raised.
And yes, I know I could add a warning using code line trigger_error().
In short: Put trigger_error() at the beginning of the method.
Long: You need to reflect the class, retrieve the DocComment, parse it and extract the #deprecated-tag. The problem is, that you must do this on every method call, and even if it there exists an easy way to catch every call, it would be a huge overhead.
If you are still interested in an answer:
$trace = debug_backtrace();
$trace = $trace[0];
Helper::logToFile('called deprecated method '.__ FUNCTION __.' on line '.$trace['line'].' in file '.$trace['file'], 'deprecated');
Where the log to file method could look like:
$text .= "\n";
$file = fopen('log/deprecated', 'a+');
fputs($file, $text, strlen($text));
fclose($file);
There is an RFC for it, but under discussion: #[Deprecated] Attribute. If accepted, you will be able to do this, for example:
#[Deprecated("use split() instead")]
function explode() {}
When you call it, it will emit a deprecation notice, like so:
Deprecated: Function explode() is deprecated, use split() instead
Maybe we could get this in PHP 8.2? I hope so!
may be own file parser helps you...
deprecated mean that in next version this function will be removed from code... in this case you do not need to think about E_DEPRECATED
Addendum might help, it adds annotations to PHP.
I've used exceptions in Java and like the way it won't let you call a method unless you catch or throw the exceptions that it might throw.
I'm looking for something similar in PHP. I realise PHP is more dynamic than Java, and doesn't even let you define the exceptions that it throws, but what is the closest I can get?
We document our methods using PHP Doc, so something that triggered an E_WARNING if you called a method without the correct try/catch block, or threw an exception without the correct #thows comment, would be perfect.
There's no way to do it in PHP itself. You will have to parse PHP and figure it out yourself. Try writing phc plugin for this.
I don't think you can reasonably get very close at all, because the language core provides just nothing whatsoever for you to work with. At best, you'd wind up creating some kind of entirely user-space funcall/exception validation mechanism that would have an absolutely horrific impact on performance.
I'm not sure that you can accomplish your stated goal. The PHP environment doesn't analyze what a function might or might not do, which would generally be a compile-time operation for other languages (I would think). I don't think you can even find that sort of thing via Reflection.
You are wrong however when you say that you can't define the exceptions that get thrown, as the Exception base class is fully extendable. PHP doesn't throw any exceptions by default though, it triggers errors. There is a fundamental difference between triggered errors and Exceptions, the latter being a user-land construct for the most part.
This isn't your question, but I'd put out a suggestion that if you wanted to move to a fully Exception-oriented environment, you could write your own error handler using set_error_handler() and manage the PHP triggered errors, and have it throw out an Exception.
I think you can simply reproduce this behavior in PHP using exception handlers and reflection.
class YourException extends Exception {
public function __toString() {
return __CLASS__;
}
}
class MyObject {
public function __construct(){}
/**
* #throws MyException
*/
public function myMethod() {
return 'foo';
}
}
try {
$o = new MyObject();
$o->myMethod();
}
catch(YourException $e) {
$method = new ReflectionMethod('MyObject', 'myMethod');
$method->getDocComment();
$throws = // Get the #throws comment (foreach, regexp, whatever);
if($e === $throws) {
// do something
}
}
Setting your own exception handler.
Grab and analyse the comments with Reflection mechanism (see getDocComment)
At the team with which I work, we have an old codebase using PHP's ibase_* functions all over the code to communicate with database. We created a wrapper to it that would do something else beside just calling the original function and I did a mass search-replace in the entire code to make sure that wrapper is used instead.
Now, how do we prevent usage of ibase_* functions in the future?
Preferably, I'd like to still have them available, but make it throw a NOTICE or WARNING when it is used.
A solution in pure PHP (not needing to compile a custom version of PHP) is preferred.
trigger_error()
function my_deprecated_function() {
trigger_error("Deprecated function called.", E_USER_NOTICE);
// do stuff.
}
Generally speaking you can flag a method as deprecated to give your users warnings about code that will not work in future versions. I think the best way is to use trigger_error along with some phpdoc.
/**
* #deprecated
*
* #return $this
*/
public function oldMethod()
{
trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
return $this;
}
The #deprecated phpdoc is important because many IDEs like PHPStorm recognise it and strike the method name if you try to use it, so you notice that is deprecated before actually running your code.
It will look more or less like this:
Beside the phpdoc you can make sure the user gets a warning by triggering the right error at runtime. Just make sure you use the right constant (i.e. E_USER_DEPRECATED).
E_DEPRECATED is instead used internally by PHP thus you should not be using it. More information here.
If I understand correct, you want to trigger an error when a built-in PHP function is used? In that case, take a look at the Override Function function.
I haven't checked it by myself, but found this in my bookmarks: http://wiki.php.net/rfc/e-user-deprecated-warning
Edit: Okay this doesn't work yet - so instead of E_USER_DEPRECATED just use something like E_USER_NOTICE:
<?php
class Foo
{
public function __construct()
{
trigger_error('Use Bar instead', E_USER_NOTICE);
}
}
$foo = new Foo()
This will end up with this:
Notice: Use Bar instead in /home/unexist/projects/ingame/svn/foo.php on line 6
If your functions are part of a class, then you could use trigger_error in the constructor to warn of the deprecation.
Alternatively, if the functions are in a single file, then triggering a deprecation warning at the top of the file would show the error whenever the file is included elsewhere.
Finally, you could throw the error on the first line of any of the deprecated functions.
Instead of raising a runtime warning on usage, you could consider writing a script, that can scan your code base for the use of this function, then generate a report of offending code. Once in a while, run it through.
If you use a version control system, you could set the script as a commit-hook. I would probably recommend a post-hook, that simply sends an email, when a script, containing deprecated functions, is checked in, but if you really want to enforce it, you could have a pre-hook completely prevent anyone from checking it in.