DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Are PHP objects passed by reference ?
Nicolas Bailly
Nicolas Bailly

Posted on • Updated on

Are PHP objects passed by reference ?

TL;DR : No.

For the longest time I've considered that Objects were "passed by reference" in PHP, and while it may not be technically true, it's a useful simplification that does the job in most cases. So if you read the title and thought "Yes they are" there's no need to panic, everything is probably fine, it doesn't mean that your apps will suddenly blow in your face. On the other hand if you thought "what on earth is a reference ?", you miiight have a few surprises lurking in your codebase, and you're definitely in the right place !

So let's dig in with a refresher on references, then we'll see how it looks like objects are passed by reference, and then why it's actually a bit more complicated than that.


References in PHP

The PHP doc about references tells us that references are "aliases", which means that we can tell PHP we want two variables to point to the same data (the docs also tells us that despite appearances they are not pointers, which is not particularly helpful unless you're already proficient in C). The symbol to create a reference is &.

So we can do :

$a = 'foo'; // $a contains 'foo'
$b = &$a;   // $b is now a reference, or an alias of $a

$b = 'bar' // We're actually changing the content of $a
echo $a    // displays 'bar' (because we just changed the value)
echo $b    // displays 'bar' (because it's still a reference to $a)
Enter fullscreen mode Exit fullscreen mode

This is a nice little oddity, but really not that useful in your everyday code. Now what's a lot more useful is that you can pass variables by reference to a function, so that the function can actually modify the content of the variable outside of its own scope.

This is done by appending the & in front of the parameter in the function declaration.

/**
 * Decrements a number if it's > 0.
 * Returns true if it was decremented, false if it was already 0 or negative;
 */
function decrement(int &$a) {
    if ($a > 0) {
        $a = $a - 1;
        return true;
    }
    return false;
}

$timeLeft = 10;
$wasModified = decrement($timeLeft);

echo $timeLeft; // will output 9
Enter fullscreen mode Exit fullscreen mode

As you can see, this allows us to return something, and modify what was passed to the function directly. This is notably used in the preg_match() function that will return true if something matched, and accept a parameter $m that will be modified to contain what was matched. This is a useful feature, but it could also be the cause of hard to track bugs, so you should probably use this with caution.

Here's a nice animation from this post that illustrates it nicely :

Animation of a coffee cup that gets duplicated then filled. When passed by reference the original cup gets filled to


Why objects look like they're passed by reference

It's likely that you've already read somewhere that objects are always passed by reference in PHP. And it sure looks like it.

Take this simple example :

function doStuff($object) {
    $object->data = 'newValue';
}

$object = (object)['data' => 'value'];
doStuff($object);

echo $object->data; // displays 'newValue'
Enter fullscreen mode Exit fullscreen mode

As you can see, there's no & in our function declaration, and yet $object->data now contains 'newValue' instead of 'value'. So the function did modify the object, which indeed looks a whole lot like it was passed by reference.

The issue with modifying objects

Here's a more concrete example that is the source of bugs in actual production codebases. Let's say we want to send our users an email with a receipt for the coming month, and we want the title to display the beginning of the period and the end of the period in a user friendly manner like "From Thursday July 1 to Saturday July 31", and the current date in the footer of the email. We'll create some functions to get the start and end of the month corresponding to the current date, and format them, then use the result in a string (or more realistically a template). It would look like this :

function getFirstDay($date) {
    return $date->modify('first day of this month')->format('l F j');
}

function getLastDay($date) {
    return $date->modify('last day of this month')->format('l F j');
}

$today = new DateTime('2022-07-15');
$firstday = getFirstDay($today);
$lastDay = getLastDay($today);

$emailTitle = "Your receipt for the period from $firstday to $lastDay";
$emailFooter = "Sent on " .  $today->format("l F j");
Enter fullscreen mode Exit fullscreen mode

The title will say
"Your receipt for the period from Thursday July 1 to Saturday July 31", which is what we want, but the footer says "Sent on Saturday July 31", which is definitely not what we want. The issue is that $today was actually modified by the functions when we called ->modify() on the date, so as soon as we use this on a date we lose the original value of the date forever...

There are two ways around this :

1- clone the object before working on it :

function getFirstDay($today) {
    $date = clone $today;
    return $date->modify('first day of this month')
        ->format('l F j');
}
Enter fullscreen mode Exit fullscreen mode

2- Use a library like Carbon 2 that provides Immutable Objects, which are essentially objects where every method always returns a clone instead of modifying the current object.


Why objects aren't really passed by reference

So we've seen that it looks an awful lot like objects are passed by reference and how to avoid potential issues, but the whole point of this post was to show that they're not, so here are a couple examples that make it obvious.

Example 1 : objects in an array

Let's assume a User class that takes the name of the user in the constructor and assigns it to a name property. Now let's see what happens if we put several of those users in an array and pass it to a function that makes the name uppercase for each element :

Class User
{
    public function __construct(public string $name){}
}

function capitalizeNames(array $users) {
    foreach ($users as $user) {
        $user->name = strtoupper($user->name);
    }
}

$users = [
    new User('John'),
    new User('Jack'),
];

capitalizeNames($users);

echo json_encode($users); // [{"name":"JOHN"},{"name":"JACK"}]
Enter fullscreen mode Exit fullscreen mode

As you can see, each user in the array was modified by the capitalizeNames() function.

We didn't use a reference, we never passed an object to the function since we passed an array (by value) so we might think we're safe. And yet every object inside of the array was modified.

Example 2 : Assigning an object to another a variable

Now let's look at what happens if we assign an instance of an object to a new variable :


$user1 = new User('name');
$user2 = $user1;

$user1->name = 'Jack';

echo $user1->name; // Jack
echo $user2->name; // Jack
Enter fullscreen mode Exit fullscreen mode

So basically as soon as we do $user2 = $user1;, we can use both interchangeably and changing one will change the other.

So what happened ?

The thing is that a variable never "contains" an object. Whenever we call new, PHP will return a handle, which is a reference... a pointer... yeah let's stick with handle. That handle is basically a number that identifies the object.
You can actually see this object identifier when using var_dump(), it's the number with a # in front when dumping an object, here with var_dump($user1, $user2) from our previous example, we can see that they're both the same object since they have the same handle :

Screenshot of a var_dump that shows the object handle

You could also use spl_object_id() which returns this object identifier.

Whenever we copy an object variable, pass it to a function or put it in an array, we're not passing or copying the instance of the object, only its handle. So an object will only ever exist once unless we use clone. Kind of like an object in real life.


Conclusion

While it might be useful to think that objects are passed by reference, I think it's better to keep in mind that when you create an object there's only one instance that exists, and what you're using is not the object but simply a handle.

I hope this can be useful to someone. Also I don't have any background in C and only a vague understanding of how this is actually implemented in the PHP core. So if there's something blatantly wrong above please let me know in the comments.

Top comments (3)

Collapse
 
dakujem profile image
Andrej Rypo

It is called an "object handle", technically. One can inspect the handle of an object by calling spl_object_id.

The handle is a form of a pointer. Different from those in C, where pointers are memory addresses. In PHP they are implemented as "counted references".

"Objects are passed by reference by default" is a simplification hiding the actual implementation complexities. The "object handles" are very similar in behaviour to regular references to values. Read more in Objects and references.

Collapse
 
nicolus profile image
Nicolas Bailly

Thanks, I didn't know about spl_object_id() (it's actually more straightforward than using var_dump() as I suggested), I added it below the var_dump example.

The "Objects and references" page from the doc is what prompted me to write this article because I found it a little obtuse for a junior dev, and it didn't provide examples of situations where the "objects are passed by reference" simplification doesn't cut it.

And thanks for the "counted references" article, I just fell into a new rabbit hole :-p

Collapse
 
dakujem profile image
Andrej Rypo

It's a bit complex, but simply by understanding the concept of counted references, one can get a hint on how garbage collectors work and also understand the meaning behind WeakMap and WeakReference.
The concept is also used in ther languages that use garbage collection.

We want your help! Become a Tag Moderator.
Fill out this survey and help us moderate our community by becoming a tag moderator here at DEV.