I have a couple of libraries that use code similar to the following one.
$args = array_merge(array(&$target, $context), $args);
$result = call_user_func_array($callback, $args);
The code is different in both the cases, but the code I shown is what essentially is done. The $callback function uses the following signature:
function callback(&$target, $context);
Both the libraries document that, and third-party code (call it plug-in, or extension) adopts that function signature, which means none of the extensions defines the callback as, e.g., function my_extension_loader_callback($target, $context).
What confuses me are the following sentence in the documentation for call_user_func_array().
Before PHP 5.4, referenced variables in param_arr are passed to the function by reference, regardless of whether the function expects the respective parameter to be passed by reference. This form of call-time pass by reference does not emit a deprecation notice, but it is nonetheless deprecated, and has been removed in PHP 5.4. Furthermore, this does not apply to internal functions, for which the function signature is honored. Passing by value when the function expects a parameter by reference results in a warning and having call_user_func() return FALSE.
In particular, the highlighted sentence seems to suggest that is not done for functions define in PHP code.
Does using call_user_func_array() in this way work in PHP 5.4?
When using call_user_func_array, passing by value when a function expects a reference is considered an error, in newer versions of PHP.
This was valid PHP code before PHP 5.3.3:
//first param is pass by reference:
my_function(&$strName){
}
//passing by value, not by reference, is now incorrect if passing by reference is expected:
call_user_func_array("my_function", array($strSomething));
//correct usage
call_user_func_array("my_function", array(&$strSomething));
The above pass by value is no longer possible without a warning (my project is also set to throw exceptions on any kind of error (notice, warning, etc).) so I had to fix this.
Solution
I've hit this problem and this is how I solved it (I have a small RPC server, so there is no such thing as referenced values after deserializing params):
//generic utility function for this kind of situations
function &array_make_references(&$arrSomething)
{
$arrAllValuesReferencesToOriginalValues=array();
foreach($arrSomething as $mxKey=>&$mxValue)
$arrAllValuesReferencesToOriginalValues[$mxKey]=&$mxValue;
return $arrAllValuesReferencesToOriginalValues;
}
Although $strSomething is not passed by reference, array_make_references will make it a reference to itself:
call_user_func_array("my_function", array_make_references(array($strSomething)));
I think the PHP guys were thinking of helping people catch incorrectly called functions (a well concealed pitfall), which happens often when going through call_user_func_array.
If call_user_func_array() returns false you have a problem, otherwise everything should be fine.
Parameters aren't passed by reference by default anymore, but you do it explicitly.
The only trouble could be that your reference gets lost during array_merge(), haven't tested that.
I've found this same problem when upgrading to PHP5.4 when there were several sites using call_user_func_array with arguments passed by reference.
The workaround I've made is very simple and consists on replacing the call_user_func_array itself with the full function call using eval(). It's not the most elegant solution but it fits the purpose for me :)
Here's the old code:
call_user_func_array($target, &$arguments);
Which I replace with:
$my_arguments = '';
for ($i=0; $i<count($arguments); $i++) {
if ($i > 0) { $my_arguments.= ", "; }
$my_arguments.= "\$arguments[$i]";
}
$evalthis = " $target ( $my_arguments );";
eval($evalthis);
Hope this helps!
Related
I have several functions in my code, who require more than 1 arguments.
I was using PHP 5.5, and whenever I input only the first argument, it worked fine, and simply ignored the second argument (I guess it saw it as "0").
But I've just upgraded to PHP 7.2 and this has become an issue.
So instead of fixing all of my code, which is spread on multiple files, is there a way to order PHP 7.2 to keep treating undefined arguments as "0", in order to avoid this Error, which is causing the page to crash?
EDIT:
sample function:
function calculate( $m,$status ) {
return ''.$m.''.$status.'';
}
if $status doesn't exist, it should simply output $m
Easiest and cleanest way is to provide a default value for each parameter, making them optionals:
function myFunction ($requiredParam, $optionalInt = 0, $optionalStr = '') {
//
}
then you can call myFunction(1); or myFunction(1,2); myFunction(1,2,'x'); without errors.
If you're curious about why this happens, it's beacuse
"Previously, a warning would be emitted for invoking user-defined
functions with too few arguments. Now, this warning has been promoted
to an Error exception. This change only applies to user-defined
functions, not internal functions."
Is there any way to easily fix this issue or do I really need to rewrite all the legacy code?
PHP Fatal error: Call-time pass-by-reference has been removed in ... on line 30
This happens everywhere as variables are passed into functions as references throughout the code.
You should be denoting the call by reference in the function definition, not the actual call. Since PHP started showing the deprecation errors in version 5.3, I would say it would be a good idea to rewrite the code.
From the documentation:
There is no reference sign on a function call - only on function definitions. Function definitions alone are enough to correctly pass the argument by reference. As of PHP 5.3.0, you will get a warning saying that "call-time pass-by-reference" is deprecated when you use & in foo(&$a);.
For example, instead of using:
// Wrong way!
myFunc(&$arg); # Deprecated pass-by-reference argument
function myFunc($arg) { }
Use:
// Right way!
myFunc($var); # pass-by-value argument
function myFunc(&$arg) { }
For anyone who, like me, reads this because they need to update a giant legacy project to 5.6: as the answers here point out, there is no quick fix: you really do need to find each occurrence of the problem manually, and fix it.
The most convenient way I found to find all problematic lines in a project (short of using a full-blown static code analyzer, which is very accurate but I don't know any that take you to the correct position in the editor right away) was using Visual Studio Code, which has a nice PHP linter built in, and its search feature which allows searching by Regex. (Of course, you can use any IDE/Code editor for this that does PHP linting and Regex searches.)
Using this regex:
^(?!.*function).*(\&\$)
it is possible to search project-wide for the occurrence of &$ only in lines that are not a function definition.
This still turns up a lot of false positives, but it does make the job easier.
VSCode's search results browser makes walking through and finding the offending lines super easy: you just click through each result, and look out for those that the linter underlines red. Those you need to fix.
PHP and references are somewhat unintuitive. If used appropriately references in the right places can provide large performance improvements or avoid very ugly workarounds and unusual code.
The following will produce an error:
function f(&$v){$v = true;}
f(&$v);
function f($v){$v = true;}
f(&$v);
None of these have to fail as they could follow the rules below but have no doubt been removed or disabled to prevent a lot of legacy confusion.
If they did work, both involve a redundant conversion to reference and the second also involves a redundant conversion back to a scoped contained variable.
The second one used to be possible allowing a reference to be passed to code that wasn't intended to work with references. This is extremely ugly for maintainability.
This will do nothing:
function f($v){$v = true;}
$r = &$v;
f($r);
More specifically, it turns the reference back into a normal variable as you have not asked for a reference.
This will work:
function f(&$v){$v = true;}
f($v);
This sees that you are passing a non-reference but want a reference so turns it into a reference.
What this means is that you can't pass a reference to a function where a reference is not explicitly asked for making it one of the few areas where PHP is strict on passing types or in this case more of a meta type.
If you need more dynamic behaviour this will work:
function f(&$v){$v = true;}
$v = array(false,false,false);
$r = &$v[1];
f($r);
Here it sees that you want a reference and already have a reference so leaves it alone. It may also chain the reference but I doubt this.
In PHP, objects are effectively passed by reference (what’s going on under the hood is a bit more complicated). Meanwhile, parameters to call_user_func() not passed by reference.
So what happens with a piece of code like this?
class Example {
function RunEvent($event) {
if (isset($this->events[$event])) {
foreach ($this->events[$event] as $k => $v) {
//call_user_func($v, &$this);
// The above line is working code on PHP 5.3.3, but
// throws a parse error on PHP 5.5.3.
call_user_func($v, $this);
}
}
}
}
$e = new Example;
$e->events['example'][] = 'with_ref';
$e->events['example'][] = 'without_ref';
$e->RunEvent('example');
function with_ref(&$e) {
$e->with_ref = true;
}
function without_ref($e) {
$e->without_ref = true;
}
header('Content-Type: text/plain');
print_r($e);
Output:
Example Object
(
[events] => Array
(
[example] => Array
(
[0] => with_ref
[1] => without_ref
)
)
[without_ref] => 1
)
Note: Adding error_reporting(E_ALL); or error_reporting(-1); to the top of the file makes no difference. I’m seeing no errors or warnings, and of course php -l on the command line shows no errors.
I was actually expecting it to work both with and without references in the callback functions. I thought that removing the ampersand before $this in call_user_func() would be enough to fix this for the latest version of PHP. Obviously, the version with the reference doesn’t work, but equally it doesn’t throw any linting errors, so it’s hard to track down such situations (which may occur many times in the codebase I’m working with).
I’ve got a practical question here: Is there any way to make the with_ref() function work? I’d like to modify only the RunEvent() code, not every single function which uses it (the majority of which do use references).
I’ve also got a curiosity question, as the behaviour I see here makes no sense to me. The opposite would make more sense. What’s actually going on here? It seems startlingly counter-intuitive that a function call without an ampersand can modify the object, while one with the ampersand cannot.
Parameters passing
The main issue is - that parameters, passed to call_user_func() will be passed as values - so they will be copy of actual data. This behavior overrides the fact, that
objects are passed by reference. Note:
Note that the parameters for call_user_func() are not passed by
reference.
Tracking error
You're not fully correct about "silent agreement" in such cases. You will see error with level E_WARNING in such cases:
Warning: Parameter 1 to with_ref() expected to be a reference, value given in
So - you will be able to figure out that you're mixing reference and values passing
Fixing the issue
Fortunately, it's not too hard to avoid this problem. Simply create reference to desired value:
class Example {
function RunEvent($event) {
if (isset($this->events[$event])) {
foreach ($this->events[$event] as $k => $v) {
$obj = &$this;
call_user_func($v, $obj);
}
}
}
}
-then result will be quite as expected:
object(Example)#1 (3) {
["events"]=>
array(1) {
["example"]=>
array(2) {
[0]=>
string(8) "with_ref"
[1]=>
string(11) "without_ref"
}
}
["with_ref"]=>
bool(true)
["without_ref"]=>
bool(true)
}
Obviously, the version with the reference doesn’t work, but equally it doesn’t throw any linting errors, so it’s hard to track down such situations (which may occur many times in the codebase I’m working with).
It throws an error: Warning: Parameter 1 to with_ref() expected to be a reference....
Error_reporting while developing should be error_reporting(-1);.
I’ve got a practical question here: Is there any way to make the with_ref() function work? I’d like to modify only the RunEvent() code, not every single function which uses it (the majority of which do use references).
You can replace call_user_func($v, $this); with $v($this);.
I’ve also got a curiosity question, as the behaviour I see here makes no sense to me. The opposite would make more sense. What’s actually going on here?
call_user_func can only pass parameters by value, not by reference.
Why does the error "expected to be a reference, value given" appear?
Here’s the modified code (taken from the accepted answer) and comment (taken from the other answers and a comment on the accepted answer). The answer to part 2 of this question is buried in a comment on the accepted answer. If the whole answer were in one place I’d just accept it and have done with it, but since I’ve stitched it together I’m throwing it in here.
function RunEvent($event) {
if (isset($this->events[$event])) {
foreach ($this->events[$event] as $v) {
$obj = &$this;
call_user_func($v, $obj);
// The user func *should* receive the object by value, not
// by reference. What's *actually* passed is a pointer to
// the location of the object, so modifications to the object
// in that func will actually be applied to the real object.
// In that case, a simple call_user_func($v, $this) will
// work.
//
// However, some of the existing user funcs receive the
// object by reference. That can and should be changed,
// but there are quite a lot to track down, and they don't
// throw linting errors. In that case, call_user_func will
// pass by value, and they're expecting to recive it by
// reference, so you get a run-time warning. (In theory,
// anyway. When I was practising on a standalone script
// I saw no warnings at all.) That's not good.
//
// One way around this would be to use $v($this) instead
// of call_user_func, but the user func may sometimes be
// a class method, so that's not going to work either. Instead,
// the above compromise method seems to work without problems.
//
// We may at some future point switch to call_user_func($v, $this),
// and track down the remaining warnings as we hit them.
//
// https://stackoverflow.com/q/20683779/209139
}
}
}
First of all, "objects" are not "passed by reference" or "effectively passed by reference" (that's a made-up term). "Objects" are not values in PHP5, and cannot be "passed" at all. The value of $e, $this, etc., is a pointer to an object.
In PHP, things are passed by reference when there is a &, and passed by value otherwise. Always.
call_by_func is just a function. In its declaration, its parameter does not have a &. Therefore, that parameter is pass-by-value. Therefore, what you passed to call_by_func is always passed by value.
You were using "call-time pass-by-reference" in PHP 5.3, which overrode a pass-by-value parameter into pass-by-reference, and was really bad practice and was removed in PHP 5.4.
Neither the functions with_ref and without_ref assign to their parameter ($e), so actually there was no point to pass it by reference. But since you declared the parameter to with_ref as pass-by-reference, there is a problem when using it with call_user_func, because as we discussed before, call_user_func takes its parameter by value, so it already lost the "reference", so there's no way it can pass-by-reference to your function. The documentation of call_user_func says it results in a warning and the call returns FALSE.
One solution, of course, it just to use $v($this);. i.e. not use call_user_func at all -- just use the name to call it directly. There is rarely a need to use call_user_func anyway.
If you must use the call_user_func* family of functions, you can use call_user_func_array with an array with elements that are by reference (Remember that you can put references into an array). That preserves the "reference" so that it can be passed to the pass-by-reference function:
call_user_func_array($v, array(&$this));
Note: Before PHP 5.4, this used to do call-time pass-by-reference. However, since PHP 5.4, this is not a call-time pass-by-reference. When we use it to call by reference a function that was meant to be called by reference, it works. When we use it to call a pass-by-value function, it works as pass-by-value.
While working with Laravel framework, more specific - Form macros, I stumbled upon a weird error.
At first, I thought it's something wrong with Laravel, but then I took everything out of context:
<?php
// placeholder function that takes variable as reference
$function = function(&$reference)
{
// append to variable
$reference = $reference . ':' . __METHOD__;
};
// test with straight call
$variable = 'something';
$function($variable);
echo $variable;
// test with call_user_func(), that gets called in Laravels case
$variable = 'something'; // reset
call_user_func($function, $variable);
echo $variable;
While the first call to $function executes properly, the second try with call_user_func(), produces (excerpt from Codepad):
Warning: Parameter 1 to {closure}() expected to be a reference, value given
PHP Warning: Parameter 1 to {closure}() expected to be a reference, value given
Fiddle: Codepad # Viper-7
While writing this, I thought about call_user_func_array(): fiddle here, but the same error is produced.
Have I got something wrong about references or is this a bug with PHP?
I would call this a bug with PHP, although it's technically a bug with call_user_func. The documentation does mention this, but perhaps not in a very enlightening way:
Note that the parameters for call_user_func() are not passed by
reference.
It would be perhaps clearer to say that the arguments to call_user_func() are not passed by reference (but note that technically it's not necessary to say anything at all; this information is also embedded in the function signature).
In any case, this means is that when call_user_func finally gets to invoking its target callable, the ZVAL (PHP engine internal data structure for all types of values) for the argument being passed is not marked as "being-a-reference"; the closure checks this at runtime and complains because its signature says that the argument must be a reference.
In PHP < 5.4.0 it is possible to work around this by using call-time pass by reference:
call_user_func($function, &$variable);
but this produces an E_DEPRECATED warning because call-time pass by reference is a deprecated feature, and will flat out cause a fatal error in PHP 5.4 because the feature has been removed completely.
Conclusion: there is no good way to use call_user_func in this manner.
This works:
call_user_func_array($function, array(&$variable));
I used this code
<?php
$myfunction = function &($arg=3)
{
$arg = $arg * 2;
return $arg;
};
echo $myfunction();
?>
Worked like a charm. :)
What happens if you do this?
call_user_func($function, &$variable);
Why don't the function handling functions like call_user_func() support passing parameters by reference?
The docs say terse things like "Note that the parameters for call_user_func() are not passed by reference." I assume the PHP devs had some kind of reason for disabling that capability in this case.
Were they facing a technical limitation? Was it a language design choice? How did this come about?
EDIT:
In order to clarify this, here is an example.
<?php
function more(&$var){ $var++; }
$count = 0;
print "The count is $count.\n";
more($count);
print "The count is $count.\n";
call_user_func('more', $count);
print "The count is $count.\n";
// Output:
// The count is 0.
// The count is 1.
// The count is 1.
This is functioning normally; call_user_func does not pass $count by reference, even though more() declared it as a referenced variable. The call_user_func documentation clearly says that this is the way it's supposed to work.
I am well aware that I can get the effect I need by using call_user_func_array('more', array(&$count)).
The question is: why was call_user_func designed to work this way? The passing by reference documentation says that "Function definitions alone are enough to correctly pass the argument by reference." The behavior of call_user_func is an exception to that. Why?
The answer is embedded deep down in the way references work in PHP's model - not necessarily the implementation, because that can vary a lot, particularly in the 5.x versions. I'm sure you've heard the lines, they're not like C pointers, or C++ references, etc etc... Basically when a variable is assigned or bound, it can happen in two ways - either by value (in which case the new variable is bound to a new 'box' containing a copy of the old value), or by reference (in which case the new variable is bound to the same value box as the old value). This is true whether we're talking about variables, or function arguments, or cells in arrays.
Things start to get a bit hairy when you start passing references into functions - obviously the intent is to be able to modify the original variables. Quite some time ago, call-time pass-by-reference (the ability to pass a reference into a function that wasn't expecting one) got deprecated, because a function that wasn't aware it was dealing with a reference might 'accidentally' modify the input. Taking it to another level, if that function calls a second function, that itself wasn't expecting a reference... then everything ends up getting disconnected. It might work, but it's not guaranteed, and may break in some PHP version.
This is where call_user_func() comes in. Suppose you pass a reference into it (and get the associated the call-time pass-by-reference warning). Then your reference gets bound to a new variable - the parameters of call_user_func() itself. Then when your target function is called, its parameters are not bound where you expect. They're not bound to the original parameters at all. They're bound to the local variables that are in the call_user_func() declaration. call_user_func_array() requires caution too. Putting a reference in an array cell could be trouble - since PHP passes that array with "copy-on-write" semantics, you can't be sure if the array won't get modified underneath you, and the copy won't get detached from the original reference.
The most insightful explanation I've seen (which helped me get my head around references) was in a comment on the PHP 'passing by reference' manual:
http://ca.php.net/manual/en/language.references.pass.php#99549
Basically the logic goes like this. How would you write your own version of call_user_func() ? - and then explain how that breaks with references, and how it fails when you avoid call-time pass-by-reference. In other words, the right way to call functions (specify the value, and let PHP decide from the function declaration whether to pass value or reference) isn't going to work when you use call_user_func() - you're calling two functions deep, the first by value, and the second by reference to the values in the first.
Get your head around this, and you'll have a much deeper understanding of PHP references (and a much greater motivation to steer clear if you can).
See this:
http://hakre.wordpress.com/2011/03/09/call_user_func_array-php-5-3-and-passing-by-reference/
Is it possible to pass parameters by reference using call_user_func_array()?
http://bugs.php.net/bug.php?id=17309&edit=1
Passing references in an array works correctly.
Updated Answer:
You can use:
call_user_func('more', &$count)
to achieve the same effect as:
call_user_func_array('more', array(&$count))
For this reason I believe (unfoundedly) that call_user_func is just a compiler time short cut. (i.e. it gets replaced with the later at compile time)
To give my view on you actual question "Why was call_user_func designed to work this way?":
It probably falls under the same lines as "Why is some methods strstr and other str_replace?, why is array functions haystack, needle and string functions needle, haystack?
Its because PHP was designed, by many different people, over a long period of time, and with no strict standards in place at the time.
Original Answer:
You must make sure you set the variable inside the array to a reference as well.
Try this and take note of the array(&$t) part:
function test(&$t) {
$t++;
echo '$t is '.$t.' inside function'.PHP_EOL;
}
$t = 0;
echo '$t is '.$t.' in global scope'.PHP_EOL;
test($t);
$t++;
echo '$t is '.$t.' in global scope'.PHP_EOL;
call_user_func_array('test', array(&$t));
$t++;
echo '$t is '.$t.' in global scope'.PHP_EOL;
Should output:
$t is 0 in global scope
$t is 1 inside function
$t is 2 in global scope
$t is 3 inside function
$t is 4 in global scope
Another possible way - the by-reference syntax stays the 'right' way:
$data = 'some data';
$func = 'more';
$func($more);
function more(&$data) {
// Do something with $data here...
}