Arrays are a very important element in PHP. We can use them as functions’ arguments, to store and sort a collection of data, etc. They are always there for making our lives easier, but sometimes we tend to use arrays more than we should be.
If your only tool is a hammer then every problem looks like a nail.
Abraham Maslow (1966)
The problem
Imagine you are developing a new amazing feature for your project. You create a function with some parameters to configure its behavior. You start adding more and more parameters to the function and this one grows more than expected.
/**
* Short description.
*
* @param string $arg_1 The first argument.
* @param bool $arg_2 A second argument.
* @param array $arg_3 Another more argument.
*/
function my_function( $arg_1, $arg_2, $arg_3, ... ) {
// ...
}
At this point, you get to the conclusion that the function is becoming unmaintainable and you have the idea of refactoring all these parameters and passing a single one, an array.
/**
* Short description.
*
* @param array $args The function arguments.
*/
function my_function( $args = array() ) {
$args = wp_parse_args(
$args,
array(
// Default arguments.
)
);
// ...
}
The function definition has been simplified a lot, it’s easier to call and we can modify its body without affecting the arguments, but we have added extra complexity to it due to we need to validate the different provided arguments.
First of all, we use the function wp_parse_args()
to be sure some arguments are always present by adding default values.
We will also probably need to validate these values are right by applying some sanitizations, transformations, etc.
We discover soon the function grows and grows and most of its code is destined to parse its arguments and not to the logic for it was created.
Even worst, it could be possible that we need to create another function with similar logic where most of its arguments are the same as the previous function, duplicating the code that validates these arguments.
As if all these issues weren’t enough, our function evolves with new arguments and others that are no longer necessary. If you read my previous post where I talked about how to deprecate code in WordPress, you already know that we shouldn’t remove these obsolete arguments if we don’t want to break things.
If you think this scenario is quite extreme, replace the previous arguments with the different parts of a shipping address, like the Street, Postcode, State, Country, etc.
/**
* Ships the product to the specified address.
*
* @param Product $product The product.
* @param array $address The shipping address.
*/
function ship_product( $product, $address = array() ) {
$address = wp_parse_args(
$address,
array(
'street' => '',
'postcode' => '',
'state' => '',
'country' => '',
)
);
$street = $address['street'];
$postcode = $address['postcode'];
// ...
}
More questions come to my mind when I think in an address. What if I provide to the function a state without a country? Or if I enter a street with the wrong postcode? Does the address still make sense?
We see the different parts of an address are quite coupled one to each other and it requires a lot of processing before using it.
Under this situation, we start thinking that there must be a better approach to solve this problem, and definitely, there is.
Objects to the rescue
I come up with to change the array by an object that represents an Address and include all the validation and processing of its different parts inside this object.
By making this, we get some benefits:
- Avoid code duplication by locating the logic in a single class.
- Reduce the complexity of other parts of our code that make use of the Address object.
- Modify the class internals without exposing these changes outside.
- Keep backward compatibility with older versions of the class.
Here is a simplified version of this class:
/**
* Class that represents an address.
*/
class Address {
/**
* Data array, with defaults.
*
* @var array
*/
protected $data = array(
'street' => '',
'postcode' => '',
'state' => '',
'country' => '',
);
public __construct( $data = array() ) {
$this->data = $this->parse_data( $data );
}
/**
* @throws Exception When validation fails.
*/
protected function parse_data( $data ) {
// Validates the address data and thrown an exception if not valid.
return $data;
}
// ...
}
The class Address
stores in its property $data
the same information as our previous $address
array. In fact, we pass this array as a constructor argument, but this time, we immediately call the method parse_data()
to validate, sanitize, and process all the address info.
If the validation goes wrong, we throw an Exception and stop the execution of our code. We could have handled this error in a more graceful way, but throwing an exception is enough to illustrate our example.
And here is how we can use it in the function we saw before:
/**
* Ships the product to the specified address.
*
* @param Product $product The product.
* @param Address $address The shipping address.
*/
function ship_product( $product, $address ) {
// Don't validates the address info, just start using it!
$street = $address->get_street();
$postcode = $address->get_postcode();
// ...
}
We see how simple is our function ship_product()
now and how we can focus on its logic instead of processing its arguments.
You might have noticed that we made a full refactor of this function by changing the way we access the address info from $address['something']
to $address->get_something()
.
That means we need to update all parts of our code that make use of the older access method. Ups!
If there isn’t much code to update, it could be fine, but sometimes this is not possible or isn’t practical. How can we keep backward compatibility with the access to the info as an array?
The ArrayAccess interface
PHP provides a mechanism to access object properties as if this one was an array. We only need to implement the ArrayAccess
interface in our Address
class.
As the PHP documentation says, we need to implement some methods in our class. These methods are offsetGet
, offsetSet
, offsetExists
, and offsetUnset
. So, let’s do it:
/**
* Class that represents an address.
*/
class Address implements ArrayAccess {
// Skip the previous code.
/*
|------------------------------------------------
| Array Access Methods
|------------------------------------------------
|
| For backward compatibility with legacy arrays.
|
*/
/**
* OffsetExists for ArrayAccess.
*
* @param string $offset Offset.
* @return bool
*/
public function offsetExists( $offset ) {
return array_key_exists( $offset, $this->data );
}
/**
* OffsetGet for ArrayAccess.
*
* @param string $offset Offset.
* @return mixed
*/
public function offsetGet( $offset ) {
if ( $this->offsetExists( $offset ) ) {
$getter = "get_$offset";
if ( is_callable( array( $this, $getter ) ) ) {
return $this->$getter();
}
}
return null;
}
/**
* OffsetSet for ArrayAccess.
*
* @param string $offset Offset.
* @param mixed $value Value.
*/
public function offsetSet( $offset, $value ) {
if ( ! $this->offsetExists( $offset ) ) {
return;
}
$setter = "set_$offset";
if ( is_callable( array( $this, $setter ) ) ) {
$this->$setter( $value );
}
}
/**
* OffsetUnset for ArrayAccess.
*
* @param string $offset Offset.
*/
public function offsetUnset( $offset ) {
if ( $this->offsetExists( $offset ) ) {
unset( $this->data[ $offset ] );
}
}
}
As you can see, the implementation of these four methods is not too much complicated. We only need to translate the actions of exists
, get
, set
, and unset
to the $data
property.
Now, our class is completed, and we can do things like this:
// The Address class supports both kinds of access.
$street = $address->get_street();
$street = $address['street'];
// We can also set its properties like it was an array.
$address->set_postcode( '12345' );
$address['postcode'] = '12345';
// The offsetExists() method in action.
isset( $address['country'] ) // return: true
isset( $address['undef'] ) // return: false
// And the method offsetUnset().
unset( $address['state'] );
The ArrayAccess
interface has allowed us to keep the backward compatibility with our legacy code and work with the Address objects as arrays too. Great!
Conclusion
The case exposed in this post is just an example of how the usage of an array is not always the unique and the best way to solve a problem. This time, we replaced the array by an object, but it could be possible to use a different data structure or pattern.
The purpose of this post is to make you think that there is life beyond arrays and consider using other approaches when solving a problem instead of using an array as a silver bullet.
I hope I have achieved my goal. See you in the next episode.