code smell – objects vs arrays

“Code Smell” is a term that I have heard used in the past, and regularly use myself to describe code that may work fine, but just doesn’t seem as good or clean as it could be. Maybe there’s some repeated code, or unnecessary over-use of strings. Basically the code smells bad.

Use objects instead of associative arrays for working with collections of values.

Here’s why…

In Javascript, it is now commonplace to see a single object passed to a method as a collection of parameters.
This is for code flexibility, so you can call the method with as many or as few values in the object as you like, and you can build the list of values to be passed to a method over a larger area of code. It also means you can simply forward the parameters object to another method, perhaps modifying some of the parameters between calls.

So the community has accepted this is convenient way to pass parameters around – I’m not saying this should be done everywhere, in fact this should only be done when it smells right to do so… Currently one commonly used option is to use associative arrays to accomplish this. Here are some unexpected drawbacks:

  • `$arr[‘foo’]` can’t be described using `/** @var */` (or PHPStorm doesn’t support it).
  • `$arr[‘SomeVariebleName’]` won’t show the typo when you meant `$arr[‘SomeVariableName’]`
  • `$arr[‘bar’]` might be not set, and you’d have to use `isset()` or similar function to determine if it has been set.
  • `$arr[‘foo’]` is slower than `$arr->foo`. Not by a lot, but it is.
  • finding all the places in your code that `$arr[‘foo’]` is used when your array keeps getting passed around, is hard work.
  • you have no dedicated place to put functions for interacting with this array of parameters.

To fix some of these issues you might convert your array to an object using a cast:

$arr = ['foo'=>'a', 'bar'=>'b']; 
$object = json_decode(json_encode($arr), FALSE);
//or 
$object = (object) $arr;

However, you still have no dedicated space to describe the object – `$arr->foo` still has no built-in PHPDoc, no default value, and if you are likely to use the same object in multiple classes, you end up reproducing blocks of code, which also smells bad.

Here are some solutions…

class HelloWorldParams
{
    /** @var int */
    public $foo = 1;

    /** @var string */
    public $bar = 'foo';

    /** @var string */
    public $ran = 'bar';
}

$f = new HelloWorldParams();
$f->foo = 1;
$f->bar = 'Hello';
$f->ran = 'World!';

So here we’ve completely replaced using the array with using a custom object, in the same way that we would use a STRUCT in C. No methods, just a class that contains a bunch of properties. Using this object makes reading the code easier, tracing code easier, and refactoring code easier. And all it takes is a tiny little class that can be re-used elsewhere in your project.

What about when we have an array and we need to convert it?

Good question. We can add a constructor method to our class to make this easier:

class HelloWorldParams
{
    /** @var int */
    public $foo = 1;
    
    /** @var string */
    public $bar = 'foo';
    
    /** @var string */
    public $ran = 'bar';
    
    /**
     * HelloWorldParams constructor.
     *
     * @param mixed[] $arr
     */
    public function __construct(array $arr=[])
    {
        foreach($arr as $k => $v) {
            $this->$k = $v
        }
    }
}

$arr = [
    'foo' => 1,
    'bar' => 'Hello',
    'ran' => 'World!'
];
$f = new HelloWorldParams($arr);

This technique demonstrates replacing an associative array with an object, to improve performance, maintainability, readability and to reduce code duplication. That is all.

Some folks might say

you should always have setters and getters to access object properties

but these people are wrong. KISS!!

There is a time and a place for getters and setters (or accessors and mutators if you’re in academia) and this is not it. You can add them if you like, but IMHO you’re just adding unnecessary code. Remember that you’ve already improved your code with this simple step above, there’s no need to go overboard.

This is a simple class, and if you want to add complex functionality like input validation (which is one of the main reasons for using setters), then you’re better off using a validation class that validates the input data, and a hydrator class that populates the object, which is beyond the scope of this blog entry.


It is possible to reduce the code a little using PHPDoc instead of explicit properties – I advise against this as it reduces code clarity, but I’ll describe what I mean here anyway, so I can show you a slightly more unusual PHPDoc use-case:

/**
 * @property int $foo
 * @property string $bar
 * @property string $ran
 */
class HelloWorldParams{}
$f = new HelloWorldParams();

So this happens in PHPStorm:

PHPStorm recognises @property values

So the only practical difference between this and the previous code above, is that we can’t use default values for the properties unless we add them as real properties of the class. Also, parsing annotations in this class won’t work as effectively as it would for the previous example, because we are describing properties within the class docblock, instead of using dedicated docblocks for each property.

(A docblock is a `/** … */` , not to be confused with a multi-line comment `/* … */`.)


All the examples above allow you to add any property to the class, so by default it doesn’t prevent you from setting a property to the object that it doesn’t expect to have.
PHPStorm will warn you that you’re using an unexpected field, but it will still let you set the property:

PHPStorm Field not found
PHPStorm field declared dynamically

So here’s one way to prevent custom dynamic fields being set in your object, that still retains most of the above functionality:

class HelloWorldParams
{
    public $foo;
    public $bar;
    public $ran;
    /**
     * @param string $name
     * @param mixed $value
     */
    public function __set($name, $value)
    {
        if(!property_exists($this, $name)) {
            throw new InvalidArgumentException(
                "Property '$name' not present for object " . __CLASS__
            );
        }
    }
}
$f = new HelloWorldParams();
$f->abcd = "bar";

We’re using the `__set()` magic method to throw an exception if we try to set a value to a property that doesn’t exist. Simple.

Leave a comment