1

Call time pass by reference was removed in PHP 5.4. But I have a special case where it seems to be still available in PHP 8 (tried it here: www.w3schools.com):

$myVar = "original";
testFunc([$myVar]);
echo "Variable value: $myVar<br>"; // Output is:  "Variable value: original"
testFunc([&$myVar]);
echo "Variable value: $myVar<br>"; // Output is:  "Variable value: changed"

testFunc([&$undefinedVar]);
echo "Variable value: $undefinedVar<br>"; // Output is:  "Variable value: changed"
testFunc([$undefinedVar_2]);
echo "Variable value: $undefinedVar_2<br>"; // Output is:  "Variable value: "

function testFunc( array $arr ) : void
{
  if ( !is_array($arr)
    || count($arr) == 0 )
    return;
  $arr[0] = 'changed';
}

Additionally, this way I can get a C# like parameter out functionality. Maybe I am misunderstanding something.

Question:
How could I identify within "testFunc" if $arr[0] was passed by reference or normally?

Alternative question (for people who are searching this topic):
Check if variable was passed by reference.

ZTHawk
  • 83
  • 7
  • Is there a practical *need* to identify this inside the function? As the function author, you should not use it as a legitimate mechanism, and you should expect your callers to not use it either. If the caller uses it and something screws up, tough noogies on them. Either way, no need to make any special affordances for this within the function. – deceze Jan 18 '23 at 14:15
  • 1
    There was a similar question recently, although asked in a different way, which had some great discussion (if I remember correctly) but I unfortunately can't find it. The gist is that `testFunc([&$undefinedVar])` **does not** pass the array as reference, it creates an array at the call-site that _holds_ a reference, similar to https://3v4l.org/cohI0 – Chris Haas Jan 18 '23 at 14:36
  • A dirty test can be https://3v4l.org/AkSgW – Foobar Jan 18 '23 at 14:38
  • @Chris The details for *why* can be found here: https://stackoverflow.com/a/17528610/476 – deceze Jan 18 '23 at 14:38

2 Answers2

2

The code by @Foobar pointed me to the right direction. I am using the output of var_dump to analyze it and create a data structure out of it:

class ReferenceInfo
{
    public string $Type;
    public bool $IsReference;
    /** int, float, double, bool are always initialized with default value  */
    public bool $IsInitialized;
    /** @var ?ReferenceInfo[] */
    public ?array $SubItems;

    public static function FromVariable( mixed $arr ) : ?ReferenceInfo
    {
        /** @var ReferenceInfo $rootRefInfo */
        $rootRefInfo = NULL;

        $varInfoStr = self::varDumpToString($arr);
        $varInfoStrArray = preg_split("/\r\n|\n|\r/", $varInfoStr);
        $refInfoObjectStack = [];
        $curKey = NULL;
        foreach ( $varInfoStrArray as $line ) {
            $lineTrimmed = trim($line);
            $lineTrimmedLen = strlen($lineTrimmed);
            if ( $lineTrimmedLen == 0 )
                continue;

            if ( $lineTrimmed == '}' ) {
                array_pop($refInfoObjectStack);
                $curKey = NULL;
                continue;
            }

            if ( $lineTrimmed[0] == '[' ) {
                // Found array key
                $bracketEndPos = strpos($lineTrimmed, ']');
                if ( $bracketEndPos === false )
                    return NULL;
                
                $keyName = self::convertToRealType(substr($lineTrimmed, 1, $bracketEndPos - 1));
                $curKey = $keyName;
                continue;
            }

            $parenPos = strpos($lineTrimmed, '(');
            if ( $parenPos === false ) {
                // Must be a NULL type
                $parenPos = $lineTrimmedLen;
            }

            $type = substr($lineTrimmed, 0, $parenPos);
            $isInitialized = true;
            if ( $type == 'uninitialized' ) {
                $parenEndPos = strpos($lineTrimmed, ')', $parenPos);
                if ( $parenEndPos === false )
                    return NULL;

                $type = substr($lineTrimmed, $parenPos + 1, $parenEndPos - $parenPos - 1);
                $isInitialized = false;
            }

            $refInfoObj = new ReferenceInfo();
            $refInfoObj->IsReference = str_starts_with($type, '&');
            $refInfoObj->IsInitialized = $isInitialized;
            $refInfoObj->Type = substr($type, $refInfoObj->IsReference ? 1 : 0);
            if ( $rootRefInfo == NULL ) {
                $rootRefInfo = $refInfoObj;
            } else {
                $refInfoObjectStack[count($refInfoObjectStack) - 1]->SubItems[$curKey] = $refInfoObj;
            }

            if ( $refInfoObj->Type == 'array'
                || $refInfoObj->Type == 'object' ) {
                $refInfoObj->SubItems = [];
                $refInfoObjectStack[] = $refInfoObj;
            }
        }

        return $rootRefInfo;
    }

    private static function convertToRealType( string $keyName ) : float|int|string
    {
        if ( $keyName[0] == '"' ) {
            $keyName = substr($keyName, 1, strlen($keyName) - 2);
        } else if ( is_numeric($keyName) ) {
            if ( str_contains($keyName, '.') )
                $keyName = doubleval($keyName);
            else
                $keyName = intval($keyName);
        }
        
        return $keyName;
    }

    private static function varDumpToString( mixed $var ) : string
    {
        ob_start();
        var_dump($var);
        return ob_get_clean();
    }
}
ZTHawk
  • 83
  • 7
0

This is not call-time pass by reference. The parameter to the function is a PHP array, which is passed by value.

However, individual elements of PHP arrays can be references, and the new by-value array will copy over the reference, not the value it points to. As the PHP manual puts it:

In other words, the reference behavior of arrays is defined in an element-by-element basis; the reference behavior of individual elements is dissociated from the reference status of the array container.

To see more clearly, let's look at an example with no functions involved:

$myVar = 42;
// Make an array with a value and a reference
$array = [
    'value' => $myVar,
    'reference' => &$myVar
];
// Make a copy of the array
$newArray = $array;

// Assigning to the value in the new array works as normal
// - i.e. it doesn't affect the original array or variable
echo "Assign to 'value':\n";
$newArray['value'] = 101;
var_dump($myVar, $array, $newArray);

echo "\n\n";

// Assigning to the reference in the new array follows the reference
// - i.e. it changes the value shared between both arrays and the variable
echo "Assign to 'reference':\n";
$newArray['reference'] = 101;
var_dump($myVar, $array, $newArray);

Output:

Assign to 'value':
int(42)
array(2) {
  ["value"]=>
  int(42)
  ["reference"]=>
  &int(42)
}
array(2) {
  ["value"]=>
  int(101)
  ["reference"]=>
  &int(42)
}


Assign to 'reference':
int(101)
array(2) {
  ["value"]=>
  int(42)
  ["reference"]=>
  &int(101)
}
array(2) {
  ["value"]=>
  int(101)
  ["reference"]=>
  &int(101)
}

Additionally, this way I can get a C# like parameter out functionality.

You do not need any hacks to achieve an out parameter. Pass-by-reference is still fully supported, it's just the responsibility of the function to say that it uses it, not the code that calls the function.

function doSomething(&$output) {
    $output = 'I did it!';
}

$output = null;
doSomething($output);
echo $output;

C# out parameters also need to be part of the function definition:

To use an out parameter, both the method definition and the calling method must explicitly use the out keyword.

The only difference is that PHP only has an equivalent for ref, not out and in:

[The out keyword] is like the ref keyword, except that ref requires that the variable be initialized before it is passed.

IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • Even though this answer has useful information, it does not answer my questions. Therefore I have marked my answer as a solution, as it allowes me to identify variables inside the array added by reference or by value. – ZTHawk Jan 23 '23 at 05:52