3

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.

Community
  • 1
  • 1
TRiG
  • 10,148
  • 7
  • 57
  • 107

4 Answers4

2

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?

Community
  • 1
  • 1
Glavić
  • 42,781
  • 13
  • 77
  • 107
  • 1
    Using `$v($this)` is a nice trick, though [it won't work for my purposes](http://stackoverflow.com/a/20685725/209139). +1. – TRiG Dec 19 '13 at 15:40
2

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)
}
Alma Do
  • 37,009
  • 9
  • 76
  • 105
  • 1
    It should be noted that the reason the without_ref function works regardless is because of the way that php treats references. A reference contains the information need to access the actual object as per documentation [here](http://www.php.net/manual/en/language.oop5.references.php). This means that call_user_func passes the $this reference as a value to the without_ref function(which works fine because without_ref is expecting a value and not a reference) but since the parameter passed contains the information to access the original object the function is able to modify the original object. – elitechief21 Dec 19 '13 at 14:49
  • Aha! @elitechief21 that actually makes some sense. Thank you. In combination with the answer above, it fixes my problems. – TRiG Dec 19 '13 at 15:23
  • "objects are passed by reference. Note:" "Objects" are never "passed by reference" in PHP. "Objects" are not values in PHP. Everything in PHP is pass by value if there is no `&` on the parameter, and pass by reference if there is a `&` on the parameter. Period. – newacct Dec 20 '13 at 07:26
1

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
        }
    }
}
Community
  • 1
  • 1
TRiG
  • 10,148
  • 7
  • 57
  • 107
1

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.

newacct
  • 119,665
  • 29
  • 163
  • 224