Search code examples
phpfunctionargumentsoptional-arguments

how to avoid the disadvantages of positional arguments in php's functions?


I have some php functions that have a huge number of parameters with default values. Something like this:

function foo($a=0,$b=6,$c=2,...

I wonder what can I do to avoid the horrors of positional arguments. Is it possible to call the function with just a subset of the arguments set, and by name? As in:

foo($c=37)

thanks!


Solution

  • I absolutely understand where this question is comming from, but I must say: if your function requires a series of arguments, that are optional, 9/10 there's something wrong with your function.
    A function in real life is the ability to do something. In the real world, a chair is something to sit on. It can have N legs, but it does a single job. In a lot of code I've seen over time, functions that have too many optional arguments do different tasks, depending on what arguments are passed to it. Thus, that function has more than 1 task. That's not what functions are for!

    Rethink your logic, please. If, however your function does a single job, irrespective of which arguments are specified @OZ_'s answer is the one to go for: type-hinting (via Class-name or interface) is the right way to do this.
    Associative arrays would work, too, but to avoid having to write a ton of if's like:

    $a = 'default';
    if (isset($args['a']))
    {
        $a = $args['a'];
    }
    $b = isset($args['b']) ? $args['b'] : 'default';//or messy ternary-packed code
    

    you might choose to write something like this:

    function someF(array $args)
    {
        $a = $b = $c = $d = null;//all your arguments and default values
        foreach($args as $name => $val)
        {
            $$name = $val;
        }
        //function body
    }
    someF(range(1,3));//works, but sets ${'0'}, ${'1'}, ...
    someF(array('a' => 1));//works fine
    someF(array('a' => new stdClass));//is $a going to be the right type?
    

    Still, as in the if's/ternary approach, You can't predict which types the values will be, nor do you know what type of array is being passed (assoc/numeric) so you might end up setting lots of variables you don't need. Plus, would you call this code easy to read/debug/maintain a couple of months from now?

    Using a class, or interface, on the other hand:

    class ArgumentsForFunc
    {
        private $a = null;
        private $email = null;
        public function setA($val = null)//defaults to default value...
        {
            $this->a = $val === null ? null : (int) $val;//cast to correct type
            return $this;
        }
        public function getA()
        {
            return $this->a;
        }
        public function setEmail($email = null)
        {
            if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL))
            {//check data here!
                $email = null;
                //or better yet:
                throw new InvalidArgumentException((string) $email.' is not a vaild email address');
            }
            $this->email = $email;
            return $this;
        }
        public function getEmail($default = null)
        {
            return $this->email ? : $default;
        }
    }
    

    This class's job is clear: it holds the data, which it receives through setters. These setter methods validate the raw data, and can throw exceptions or cast to the correct type. This means you can use it like so:

    function betterF(ArgumentsForFunc $args)
    {
        $a = $args->getA();
        $email = $args->getEmail('default@email.arg');
        echo 'send ', $a, ' mails to ', $email;
    }
    $arg = new ArgumentsForFunc;
    $arg->setA(2);
    betterF($arg);//send 2 mails to default@email.arg
    

    Now you can rest assured that, as long as the user passes an instance of ArgumentsForFunc, your function will behave as you expected to. Of course, this approach requires you to write a bit more code initially, but it'll save you tons of trouble debugging, and makes testing a lot easier.

    PS: throw exceptions in data-models. If the data that is being set is invalid, notify the user ASAP. When an exception is thrown as soon as the setter is called, you can easily trace the bug. If you don't (and silently fail), you can spend hours on end trying to work out why your getEmail call is returning null, and not the email address you thought you passed to the setter somewhere else.