PHP Function Arguments - Use an array or not?

2020-05-24 05:28发布

问题:

I like creating my PHP functions using key=>value pairs (arrays) as arguments instead of individual parameters.

For example, I prefer:

function useless_func($params) {
    if (!isset($params['text'])) { $params['text'] = "default text"; }     
    if (!isset($params['text2'])) { $params['text2'] = "default text2"; }   
    if (!isset($params['text3'])) { $params['text3'] = "default text3"; }   
    echo $params['text'].$params['text2'].$params['text3'];
    return;
}

And I don't like:

function useless_func($text = "default text", $text2 = "default text2", $text3 = "default text3") {
        echo $text.$text2.$text3;
    return;
}

I had first seen things done this way extensively in the Wordpress codebase.

The reason I prefer arrays:

  • Function arguments can be provided in any order
  • Easier to read code / more self documenting (in my opinion)
  • Less prone to errors, because when calling a function I must investigate the proper array keys

I was discussing this with a co-worker and he says that it's useless and just leads to extra code and it's much harder to set the default values. Basically, he disagrees with me completely on all three points.

I am looking for some general advise and guidance from experts who might be able to provide insight: What's the better or more proper way to do this?

回答1:

Well, it's kinda usefully. But for some arguments which is passing always it's better to use classic passing like function some($a1, $a2). I'm doing like this in my code:

function getSome(SomeClass $object, array $options = array())
{
    // $object is required to be an instance of SomeClass, and there's no need to get element by key, then check if it's an object and it's an instance of SomeClass

    // Set defaults for all passed options
    $options = array_merge(array(
        'property1' => 'default1',
        'property2' => 'default2',
        ... => ...
    ), $options); 
}

So, as you can see I like that code style too, but for core-arguments I prefer classic style, because that way PHP controls more things which should I, if I used the you code style.



回答2:

Don't do that!

Passing all in an array is a bad idea most of the time.

  • It prevents people from using your function without knowing what it needs to operate.
  • It lets you create functions needing lots of parameters when probably you should create a function with more precise argument needs and a narrower goal

It seems like the contrary of injecting in a function what it needs.

Function arguments can be provided in any order

I have no such preference. I don't understand that need.

Easier to read code / more self documenting (in my opinion)

Most IDEs will present you with the different arguments a function needs. If one sees a function declaration like foo(Someclass $class, array $params, $id) it is very clear what the function needs. I disagree that a single param argument is easier to read or self documenting.

Less prone to errors, because when calling a function I must investigate the proper array keys

Allowing people to pass in an array without knowing that values will be defaulted is not close to "not error-prone". Making it mandatory for people to read your function before using it is a sure way for it never to be used. Stating that it needs three arguments along with their defaults is less error prone because people calling your function will know which values the parameters will be defaulted to, and trust that it will present the result they expect.


If the problem you are trying to solve is a too great number of arguments, the right decision is to refactor your functions into smaller ones, not hide function dependencies behind an array.



回答3:

I'm assuming you're asking whether it's A Good Thing to write all functions so that they accept only one argument, and for that argument to be an array?

If you're the only person who's ever going to work on your code then you can do what you like. However, by passing all argument values through an array, anyone else is going to have to work harder to understand what the function does and why / how they could use it, especially if they're using an IDE with auto-complete for function names etc. They don't call it a "function signature" for nothing.

I'd recommend that array parameters are reserved either for items where you don't know how many there will be (e.g. a series of data items), or for groups of related options / settings (which may be what's going on in the Wordpress example that you mention?).

If you do continue with a blanket approach to array arguments then you should at least be aware of its impact on readability and take some steps to counter that issue.



回答4:

Your co-worker is right. Not only is it more code for the same functionality, it is harder to read and probably has lowered performance (Since you need to call isset for each param and you need to access an array to set values).



回答5:

This borders on Cargo Cult programming. You say this is more readable and self-documenting. I would ask how? To know how to use your function/method I have to read into the code itself. There's no way I can know how to use it from the signature itself. If you use any half-decent IDE or editor that supports method signature hinting this will be a real PITA. Plus you won't be able to use PHP's type-hinting syntax.

If you find you are coding a load of parameters, especially optional parameters then it suggests there might be something wrong with your design. Consider how else you might go about it. If some or all of the parameters are related then maybe they belong to their own class.



回答6:

Using array_merge() works okay, but using the + operator can be used too; it works the other way, it only adds default values where one hasn't been given yet.

function useless_func(array $params = array())
{
    $params += array(
        'text' => 'default text',
        'text2' => 'default text2',
        'text3' => 'default text3',
    );
}

See also: Function Passing array to defined key

A few things you don't get with using arrays as function arguments is:

  1. type checking (only applicable to objects and arrays, but it can be useful and in some cases expected).
  2. smart(er) text editors have a code insight feature that will show the arguments a function understands; using arrays takes away that feature, though you could add the possible keys in the function docblock.
  3. due to #2 it actually becomes more error prone, because you might mistype the array key.


回答7:

Your co-worker is crazy. It's perfectly acceptable to pass in an array as a function argument. It's prevalent in many open source applications including Symfony and Doctrine. I've always followed the 2 argument rule, if a function needs more than two arguments, OR you think it will use more than two arguments in the future, use an array. IMO this allows for the most flexibility and reduces any calling code defects which may arise if an argument is passed incorrectly.

Sure it takes a little bit more work to extrapolate the values from the array, and you do have to account for required elements, but it does make adding features much easier, and is far better than passing 13 arguments to the function every time it needs to be called.

Here is a snippet of code displaying the required vs optional params just to give you an idea:

// Class will tokenize a string based on params
public static function tokenize(array $params)
{
    // Validate required elements
    if (!array_key_exists('value', $params)) {
        throw new Exception(sprintf('Invalid $value: %s', serialize($params)));
    }        

    // Localize optional elements
    $value            = $params['value'];
    $separator        = (array_key_exists('separator', $params)) ? $params['separator'] : '-';
    $urlEncode        = (array_key_exists('urlEncode', $params)) ? $params['urlEncode'] : false;
    $allowedChars     = (array_key_exists('allowedChars', $params)) ? $params['allowedChars'] : array();
    $charsToRemove    = (array_key_exists('charsToRemove', $params)) ? $params['charsToRemove'] : array();

....


回答8:

I have used arrays to substitute a long list of parameters in many occasions and it has worked well. I agree with those in this post that have mentioned about code editors not being able to provide hints for the arguments. Problem is that if I have 10 arguments, and the first 9 are blank/null it just becomes unwieldy when calling that function.

I would also be interested in hearing an how to re-design a function that requires a lot of arguments. For example, when we have a function that builds SQL statements based on certain arguments being set:

function ($a1, $a2, ... $a10){

        if($a1 == "Y"){$clause_1 = " something = ".$a1." AND ";}
        ...
        if($a10 == "Y"){$clause_10 = " something_else = ".$a10." AND ";}

        $sql = "
        SELECT * FROM some_table 
        WHERE
        ".$clause_1." 
        ....
        ".$clause_10." 
        some_column = 'N'
        ";

        return $sql;
    }

I would like to see PHP entertain adding a native helper function that could be used within a the function being called that would assist in passing an array of parameters by undertaking the necessary type checking. PHP recognized this to a certain extent by creating the func_get_args() function which allows arguments to be passed in any order. BUT this will only pass a COPY of the values, so if you want to pass objects to the function this will be a problem. If such a function existed, then the code editors would be able to pick this up and provide details on possible arguments.



回答9:

@Mike, you could also "extract()" your $params argument into local variables, like this:

// Class will tokenize a string based on params
public static function tokenize(array $params)
{
    extract($params);
    // Validate required elements
    if (!isset($value)) {
        throw new Exception(sprintf('Invalid $value: %s', serialize($params)));
    }

    // Localize optional elements
    $value         = isset($value) ? $value : '';
    $separator     = isset($separator) ? $separator] : '-';
    $urlEncode     = isset($urlEncode) ? $urlEncode : false;
    $allowedChars  = isset($allowedChars) ? $allowedChars : array();
    $charsToRemove = isset($charsToRemove) ? $charsToRemove : array();

....

Same implementation, but shorter.