I like to think I’ve gotten pretty good at PHP, but I’ll admit there are plenty of times I feel like I know nothing. One thing that has tripped me up recently is the difference between variable copies and variable references.
Often, this difference comes into play with function or method properties, but it is more than that. I hope by reading this article you will be better equipped to deal with this tricky subtilty in the language when it affects you.
References vs Copies
When we create a variable by passing an argument to a function or method, we are almost always creating a copy of that variable. But instead, we might want to just create a reference. A reference is an alias, allowing two parts of our application to act on the same variable.
This will conserve memory, though that is less of a concern today than it used to be. The real reason to use a reference is it is a useful design decision, but only sometimes.
Let’s look at the difference. First look at this class, that doesn’t use references. Guess what the output of the last line will be:
<?php $post = 1; class my_post { protected $post; public function set_post( $post ){ $this->post = $post; } public function get_post(){ return $this->post; } } $my_post = new my_post(); $my_post->set_post( $post ); $post = 2; var_dump( $my_post->get_post() );
If you guessed 1, then you are right. Just because we changed $post to 2 outside of the class scope, it shouldn’t affect the copy of $post, we put into the my_post class. But what if we wanted to? Then we would have to pass $post into the my_post class by reference and set the post property of that class by reference.
<?php $post = 1; class my_post { protected $post; public function set_post( &$post ){ $this->post = &$post; } public function get_post(){ return $this->post; } } $my_post = new my_post(); $my_post->set_post( $post ); $post = 2; var_dump( $my_post->get_post() );
The only thing that has changed here is that I put an ampersand before the $post variable. So it’s now &$post. The ampersand tells PHP to create a reference, not a copy.
As a result, the last line, now prints 2, not 1, since the reference to $post, was changed inside of the class.
Problems With Object References
Based on what you have learned, take a look at this code and guess what the last line will print:
<?php class second { public function __construct( $first ){ $first->two = 2; } } $first = new stdClass(); $first->two = 1; new second( $first ); var_dump( $first->two );
If you guessed 1, that would make sense based on what you just read, since the first object is not passed into the second object by reference. You’d also be wrong because objects are always passed by reference.
Sorry, I tricked you, but it’s an important distinction. Let’s jump into a practical example that I used to really understand the problem and look at solutions if this is not what you want.
In a recent article for Torque, I wrote a simple class for tracking hooks that were being removed. In WordPress 4.7, there is a new class WP_Hook, that is used for storing information about hooks, which is what I needed to store for removed hooks, so I could add them back later.
An earlier version of that class failed for reasons that were baffling to me until I remembered objects are always passed by reference. You can see the final version of that class here, which includes the backwards compatibility checks I have removed below for clarity’s sake. I have extracted my before and after versions so you can see the difference.
Here is what I started with:
<?php public function remove_all( $hook ){ global $wp_filter; if ( isset( $wp_filter[ $hook ] ) ) { $all = $wp_filter[ $hook ]; if( ! empty( $all ) ){ $this->removed[ $hook ] = $all; remove_all_filters( $hook ); } } }
In this version, I get an object of the WP_Hook class and place it in the $all variable, which ends up being stored in the removed property of this class. Even though I stored it in the property before calling remove_all_filters() the value in that property is still affected by calling remove_all_filters() which resets the callback property of that object because it is a reference.
To make this clearer here is a version, that doesn’t use remove_all_filters() but does what that function does manually:
<?php public function remove_all( $hook ){ global $wp_filter; $all = $wp_filter[ $hook ]; $this->removed[ $hook ] = $all; $all->callbacks = array(); }
You might look at this and think that $this->removed[ $hook ]->callbacks shouldn’t be empty, only $all->callbacks should be. But they are not two different things, one is a reference to the other.
The Clone Solution
The problem I was facing was that I needed to keep a copy of an object, before letting WordPress remove_all_filters() modify that object. Instead of keeping a copy, I was creating a reference.
The solution to the problem was to create an object clone using the clone keyword when defining the $all property. Here is what that looks like:
<?php public function remove_all( $hook ){ global $wp_filter; if ( isset( $wp_filter[ $hook ] ) ) { $all = clone $wp_filter[ $hook ]; if( ! empty( $all ) ){ $this->removed[ $hook ] = $all; remove_all_filters( $hook ); } } }
Now $all is a copy, of the object WordPress acts on with remove_all_filters(). Mission accomplished.
The Clone Magic Method
Object cloning creates a new copy of an object, but all of its properties, that are objects are still references. This is one of the reasons why PHP provides a __clone() magic method, which is called in the cloned object after the new object is cloned.
This provides an opportunity to clone any properties that may be references, if needed. Here is an example that does that:
<?php class deep_clone { public function __clone() { foreach( get_object_vars( $this ) as $property => $value ){ if( is_object( $property ) ){ $this->$property = clone $this->$property; } } } }
Beware The Attack Of The Clones
I hope this article has helped you understand the difference between references and copies in PHP and some of the tricky rules involved. Next time you don’t understand how a variable or class property suddenly changed for no apparent reason, consider that you may be dealing with a reference that changed somewhere else.
1 Comment