The codebase of a project is in continuous evolution. We add new features, fix issues, and at some point in the development, we found that some code we wrote in the past is no longer necessary. Under this situation, we may have the temptation of removing this piece of code, but is it right?
Well, it depends. If we are working on a development for a specific client, let’s say a corporate site, a WooCommerce store, or a more complex application where the production environment is well known and no one can modify our deployed code, then we can remove this obsolete code without worrying about breaking the backward compatibility.
By contrast, if we are developing a product that is used by many users in different ways and environments, then we have to be very careful with the changes we include and the code we remove. Otherwise, we might break things.
This aspect is especially important in WordPress, where the combinations of plugins, themes, and server configurations are practically infinite. Besides, we have to consider the possibility that the user has included customizations for your product thanks to the extensibility of WordPress with the usage of hooks.
To summarize:
If we are working on a WordPress plugin, theme, or a WooCommerce extension, we cannot just remove the obsolete code, we need to deprecate it.
Deprecating code is the proper way to tell the users, normally developers, to stop using a specific function, method or argument because this piece of code is obsolete and it’s going to be removed from the codebase in future releases. Optionally, you can provide an alternative, if exists.
Now we have learnt the importance of deprecating the obsolete code in our projects, let’s see how to do it:
Deprecating a function
Let’s start with something easy. We have a function that isn’t necessary anymore.
/**
* This function was very important in the past.
*
* @since 1.0.0
*
* @return string
*/
function my_plugin_old_function() {
$something = '';
// ...
return $something;
}
We can see some interesting aspects of this function. We know when it was introduced (version 1.0.0) and that it always returns a string as result.
One important thing to take into consideration when deprecating a function, and any code in general, is to keep the behavior untouched. That means the function, although deprecated, must go on doing the same task than before (if possible).
Our function has worked well all this time, but it’s the moment to deprecate it.
/**
* This function was very important in the past.
*
* @since 1.0.0
* @deprecated 1.1.0
*
* @return string
*/
function my_plugin_old_function() {
_deprecated_function( __FUNCTION__, '1.1.0', 'my_plugin_new_function' );
return my_plugin_new_function();
}
Let’s diggest the introduced changes. First of all, we have added the tag @deprecated
in the documentation of the function. This is the proper way to document when this function was deprecated.
Additionally, if you are using an IDE, you might see the calls to this function like this . This feature is really helpful while you are writing code because you can see clearly that something is wrong with this function.my_plugin_old_function()
But this tag by itself doesn’t prevent the usage of the function and it doesn’t trigger any warning on calling it. Here is where the function _deprecated_function()
enters into action.
By including the function _deprecated_function()
at the beginning of the code, we are triggering a warning every time the function is called.
Note: The function _deprecated_function()
is defined in the WordPress core. You can find more helper functions like this in the WordPress file wp-includes/functions.php.
Great, now we have our log full of warnings for using a deprecated function. How can I fix that? The answer is simple, stop using this function.
We need to know we are calling to a deprecated function and where. Here the importance of properly filling the parameters of _deprecated_function()
.
The first parameter is to indicate what function is deprecated. We can use the constant __FUNCTION__
to avoid writing the function name manually.
The second parameter indicates the version in which the function was deprecated, and the third parameter, which it’s optional, the function that replaces the old one (If there is a replacement).
Finally, the deprecated function calls to the new function to accomplish its functionality. Remember the function should continue working like before.
Note: The process is the same when we are deprecating the method of a class.
Deprecating an argument
When we want to deprecate an argument of a function or method, the process is slightly different.
Let see an example:
/**
* Short description.
*
* @since 1.0.0
*
* @param mixed $arg_1 The first argument.
* @param mixed $arg_2 The second argument.
*/
function my_plugin_function( $arg_1, $arg_2 ) {
// ...
}
Now, let’s deprecate the second argument.
/**
* Short description.
*
* @since 1.0.0
* @since 1.1.0 Deprecated `$arg_2`.
*
* @param mixed $arg_1 The first argument.
* @param mixed $deprecated Deprecated.
*/
function my_plugin_function( $arg_1, $deprecated = null ) {
if ( ! is_null( $deprecated ) ) {
_deprecated_argument('arg_2', '1.1.0', 'Passing arg_2 to my_plugin_function is deprecated.' );
}
// ...
}
The first difference between deprecating a function and deprecating an argument is that in the second we don’t use the tag @deprecated
. Obviously, we cannot use it because the function is still valid. We are deprecating one of its arguments only.
Instead, we add another @since
tag to the documentation of the function. This is a good practice. Every time we introduce important changes to the behavior of a function or method, we should add a @since
tag to log when these changes were introduced. In our case, we log that the argument $arg_2
was deprecated in version 1.1.0.
The second change we notice is that we have renamed the argument from $arg_2
to $deprecated
. This step is not mandatory, but it’s especially useful when using an IDE. For example, in PHPStorm you can preview the name of the arguments when calling a function. So, if you see that an argument is named “deprecated”, you know quickly you should ignore it.
Warning: If you are working with PHP 8+, don’t rename the arguments or you could break things due to the Named Arguments feature.
For the third change, we have set the default value of the second argument to null
. This way, we can remove it from the calls to this function.
Finally, we check when this argument is used and trigger a warning with the function _deprecated_argument()
. Another WordPress function to help us to deprecate code.
The arguments for this function are:
- The name of the deprecated argument.
- The version in which this change was introduced.
- A message to indicate what we are doing wrong.
Note: If we would like to deprecate the first parameter, the process would be the same. But don’t make it optional! Required parameters after optional parameters are not allowed in PHP 8 or higher.
A different situation is when we want to modify the accepted values for an argument. In this case, we need to make use of the function _doing_it_wrong()
.
Suppose we have an argument that used to accept a string and an array as value. Now, we want to accept an array only. So, we can do the following:
/**
* Short description.
*
* @since 1.0.0
* @since 1.1.0 The argument `$arg_1` only accepts an array as a value.
*
* @param array $arg_1 The first argument.
*/
function my_plugin_function( $arg_1 ) {
if ( ! is_array( $arg_1 ) ) {
_doing_it_wrong( __FUNCTION__, '1.1.0', 'The argument $arg_1 must be an array.' );
$arg_1 = array( $arg_1 );
}
// ...
}
Note: The accepted arguments by the function _doing_it_wrong()
are the same as the function _deprecated_function()
.
Now that we have learnt how to deprecate a function/method argument. Let’s continue with something a bit more complex.
Deprecating an object property
This time, we’re going to deprecate the property of an object. The best way to learn how to do it is with an example:
/**
* Short description.
*
* @since 1.0.0
*/
class My_Plugin_Class {
/**
* A class property.
*
* @var string
*/
public $property_1 = '';
// ...
}
In this class, we have a public property called $property_1
. That means developers can do things like that:
$plugin_class = new My_Plugin_Class();
// Directly set the property value.
$plugin_class->property_1 = 'Foo';
// Directly access the property value.
echo $plugin_class->property_1;
If we want to remove this property or change its visibility to protected
. We cannot just do that or we’ll break things. We have to keep the backward compatibility with this property. Here is how to proceed:
/**
* Class description.
*
* @since 1.0.0
*/
class My_Plugin_Class {
/**
* A class property.
*
* @var string
*/
protected $property_1 = '';
/**
* Auto-load in-accessible properties on demand.
*
* @since 1.1.0
*
* @param mixed $key Key name.
* @return mixed
*/
public function __get( $key ) {
if ( 'property_1' === $key ) {
_doing_it_wrong( 'My_Plugin_Class->property_1', '1.1.0', 'This property is no longer available. Use the method My_Plugin_Class->get_property_1() instead.' );
return $this->get_property_1();
}
}
// ...
}
There are a lot of things to explain here. First of all, we have changed the visibility of $property_1
to protected
. We could also remove this property if we don’t need it anymore.
Next, we have made use of the magic method __get()
. This method belongs to a set of “magic” methods that are available in PHP for implementing the technique of Overloading. As a resume, this method is called to process properties that are not defined or aren’t visible.
As we have changed the visibility of $property_1
to protected
, the method __get()
will be called when we’ll try to access this property directly.
And in the method __get()
is where the magic happens. Its first parameter is the name of the property we are trying to access, so we only have to catch this event and trigger our _doing_it_wrong()
method.
Additionally, we have to return the expected value for this parameter in order to keep the backward compatibility.
The same technique is applied for setting the property value, but we need to use the magic method __set()
.
Deprecating WordPress hooks
Until now, we’ve seen how to deal with obsolete PHP code in general, but it’s time to see some WordPress specific elements. I’m talking about hooks.
Hooks (filters and actions) is an essential part of the WordPress core and is what makes it so extensible and customizable.
When we develop a WordPress plugin or theme, we can add custom filters and actions to allow other developers to modify the behavior of our product or expand it with new features.
It’s very important to take our time thinking about the hooks we’re going to introduce in our code because, as you might have guessed reading this post, we have to maintain them too. We need to make our best to keep the backward compatibility with our hooks every time we modify or remove them.
Have you said to remove them? Yes, but this should happen in future releases, before that, we need to deprecate them. Let’s see how to do it:
/**
* Fired when action X happens.
*
* @since 1.0.0
*
* @param $param_1 The first parameter.
*/
do_action( 'my_custom_action', $param_1 );
In our example, we have an action hook that is triggered at some point in our code. We’ve decided that this hook is obsolete and no longer necessary.
if ( has_action( 'my_custom_action' ) ) {
_deprecated_hook( 'my_custom_action', '1.1.0', 'my_new_custom_action', 'The my_custom_action action is deprecated' );
/**
* Fired when action X happens.
*
* @since 1.0.0
* @deprecated 1.1.0
*
* @param $param_1 The first parameter.
*/
do_action( 'my_custom_action', $param_1 );
}
The code for deprecating things starts being familiar to us. Here we use the function _deprecated_hook()
, although we could use the function _doing_it_wrong()
too, we also add the tag @deprecated
in the docblock of the hook. Both elements have already been seen in this post or are self-explained, so I’m going to skip them.
What it’s new is the function has_action()
. Thanks to this function included in the WordPress core, we can know if there is any callback registered for this action hook, and in case of true, we execute our deprecated action to keep backward compatibility.
As you might have guessed, there is a similar function for filter hooks called has_filter()
.
Note: In reality, the function has_action()
is an alias of has_filter()
because, in the end, all WordPress hooks are filters! An action is nothing more than a filter that returns void
,
WooCommerce helper functions
Finally, I would like to make a special mention of some helper functions included in the WooCommerce core that are really useful when we are working with this e-commerce plugin.
I’m talking about the functions wc_deprecated_function()
, wc_doing_it_wrong()
, wc_deprecated_argument()
, etc. You can find the complete list in the file includes/wc-deprecated-functions.php.
But what makes them so interesting? Well, if you take a look at their code, you will see that they are wrappers of the functions we have seen in this post, but changing their behavior when they are called during an AJAX or a REST API request. In these kinds of requests, the deprecation warning is logged but not triggered. By doing this, we avoid breaking the returned result. Good point for the WooCommerce team!
If you are developing a WooCommerce extension, I encourage you to use these functions. If not, you can still create similar functions in your project.
In conclusion
Dealing with obsolete code is part of our work as developers, and in this post, we have learnt some techniques to deprecate these pieces of code before removing them from our project definitely.
When to remove this legacy code and where to locate it meanwhile will be discussed in another post, this one is already long enough.
Thanks to the code deprecation, we can make our project grows at the same time we keep backward compatibility with older versions. Additionally, we create a smooth transition and give enough time to other developers to migrate and adapt their WordPress sites to the new changes.
Definitely, this is a vital aspect when we are creating a product that will be used by thousands of customers around the world. Because nobody wants a product that breaks things with each update. Isn’t it?