3

I wish to give a list of options as an argument to a function.

The Ideal Scenario: Named Parameters

If PHP has named parameters it would be done like so:

function setOptions($title, $url, $public = true, $placeholder = "type here...") {
   ...
}

setOptions($title = "Hello World", $url = "example.com", $placeholder = "hi"); 

Unfortunately PHP does not have named parameters (please tell me if PHP7 is planned to have some as a comment).

The solution everyone else is using: Associative Array

Most PHP scripts I have seen use an alternative array approach like so:

function setOptions($options) {
   ...
}

setOptions(array(
   'title' => "Hello World",
   'url' => "example.com",
   'placeholder' => "hi"
));

Drawbacks of Associative Array Approach

Although this works fine, there are the following drawbacks:

  • The user does not benefit from autocompletion (taking a long time to write)
  • The user can easily makes mistakes in spellings
  • The don't know what options is available, so may frequently revert back to documentation

Is there a better way?

Is there a better way that can address these issues (either in current PHP or PHP7 or maybe even hacklang(?)).

Yahya Uddin
  • 26,997
  • 35
  • 140
  • 231

5 Answers5

3

In Hack, you can use Shapes. Shapes define a structure for associative arrays so that things can be autocompleted (depending on IDE support) and spelling mistakes are picked up by the type checker.

For instance, your example could be reworked like:

function setOptions(shape(
  'title' => string,
  'url' => string,
  'public' => ?bool,
  'placeholder' => ?string,
) $options) {
  $title = $options['title'];
  $url = $options['url'];
  $public = Shapes::idx($options, 'public', true);
  $placeholder = Shapes::idx($options, 'placeholder', 'type here...');
  ...
}

setOptions(shape(
  'title' => 'Hello World',
  'url' => 'example.com',
  'placeholder' => 'hi',
));

This marks title and url to both be required options and public and placeholder are optional (all nullable types in shapes are considered to be optional). Shapes::idx is then used to get the value provided, or the default value (the third argument) if a value was not passed in.

greenmoon55
  • 81
  • 1
  • 9
2

Solution: Using fluent setters

A potential solution I have found to this problem is to use classes and fluent setters like so:

class PostOptions {

    protected
      $title,
      $url,
      $public = TRUE,
      $placeholder = "type here..."; //Default Values can be set here

    static function getInstance(): PostOptions {
        return new self();
    }

    public function setTitle($title) {
        $this->title = $title;
        return $this;
    }

    public function setUrl($url) {
        $this->url = $url;
        return $this;
    }

    public function setPublic($public) {
        $this->public = $public;
        return $this;
    }

    public function setPlaceholder($placeholder) {
        $this->placeholder = $placeholder;
        return $this;
    }

}

You can then send the options like so:

function setOptions(PostOptions $postOptions) {
    //...
}

setOptions(
  PostOptions::getInstance()
             ->setTitle("Hello World")
             ->setUrl("example.com")
             ->setPlaceholder("hi")
);

Doing it quickly! (This looks long)

Although this may look long, it can actually be implemented VERY quickly using IDE tools.

e.g. In InteliJ or PHPStorm, just type ALT+INS > Select setters > Select the fields you want to set and check the checkbox for fluent setters > click OK

Why Fluent Setters? Why Not just make all the fields public?

Using public fields is a LOT slower. This is because fluent setters can make use of chained methods, whilst the public fields way must be written like this:

$options = new PostOptions(); 
$options->title = "hello"; 
$options->placeholder = "..."; 
$options->url "..."
setOptions($options); 

Which is a lot more typing compared to the proposed solution

Why is this better?

  • It's faster in IDE's when using autocomplete than the array approach
  • Unlikely to make mistakes in spellings (thanks to autocomplete)
  • Easy to see what options is available (again thanks to autocomplete)
  • Can give individual documentation for individual fields using PHPDoc
  • Can use nested options more easily e.g. If you had a list of options, and that option also had more list of options
  • Other OOP advantages e.g. Inheritance & Abstract Classes

How much faster is this approach?

I implemented a quick class for Wordpress labels array in: https://codex.wordpress.org/Function_Reference/register_post_type

I found that setting a property for each value (with the documentation next to you on a 2nd monitor) that the fluent setters approach is approximately 25% faster than the array approach thanks to autocomplete! However, if the documentation was not next to you, I expect this approach will far exceed 25%, as discovery of options is much quicker!

Alternative approaches are welcome

Yahya Uddin
  • 26,997
  • 35
  • 140
  • 231
  • If it's mostly about autocompletion, you could just make your options public (more pythonic code) instead of resorting to setters (which is quite the procedural approach). – mario Sep 05 '15 at 23:47
  • That's one approach, but a LOT slower. This is because fluent setters can make use of chained methods. Your way will be written like this: `$options = new PostOptions(); $options->title = "hello"; $options->placeholder = "..."; $options->url "..."` My way looks like this: `PostOptions::getInstance()->setTitle("...")->setUrl("...")->setPlaceholder('...');` – Yahya Uddin Sep 05 '15 at 23:52
  • 1
    You can have a fluent interface without copynpaste methods. I'm personally using `curl()->url($u)->verbose(D)->sslverify($o)->exec()` for example (note the lack of `set` prefixes which avoids the segregation of props and accessors). – mario Sep 06 '15 at 00:01
  • Yes you can do that! I just use the set prefix because that's what auto generated when using PHPStorm IDE features and inline with OOP best practices, but your way is good too. Won't save any additional time tough as your using autocomplete anyways, and you can skip over writing the 'set' prefix. e.g. `->title[TAB]` turns to `->setTitle()` anyways. – Yahya Uddin Sep 06 '15 at 00:05
  • Use Syntactic! I think it's a better approach to this problem https://github.com/topclaudy/php-syntactic – cjdaniel Jun 20 '16 at 16:57
  • Heavily disagree. That approach has no additional benefit to even associative arrays (in fact it's longer) and no autocomplete. – Yahya Uddin Jun 20 '16 at 17:42
  • with this solution you need to create and object to encapsulate the parameters each time you define a function (imagine you have 20+ functions). With Syntactic, you just define your function with default parameters (the normal way) and you are able to specify only the ones you want when calling your function. – cjdaniel Jun 21 '16 at 16:35
  • @Yahya Looks like autocomplete is you main concern. Not an actual solution to the problem. Some frameworks like Laravel have many interesting features without IDEs autocomplete support and developers love them. – cjdaniel Jun 21 '16 at 17:01
1

Declaration from array

This is how I normally declare my class structure. The only drawback is that it takes a while longer to write, but it allows optional parameters, defaults values, etc.

public static $defaults = array(
    'user_id' => null,
    'username' => null,
    'avatar' => null,
    'email' => null,
    'description' => null,
);

public function __construct(array $args = array()) {
    $this->dbc = Database::connection();
    $defaults = self::$defaults;
    $args = array_merge($defaults, $args);

    //Assign the object properites
    $this->user_id = (is_numeric($args['user_id'])) ? $args['user_id'] : null;
    $this->username = $args['username'];
    $this->avatar = AVATAR_DIR . $args['avatar'];
    $this->email = $args['email'];
    $this->description = $args['description'];
}

This way, you can declare an object like $x = new User(), and it will work perfectly fine. Let's say you've only selected a few columns from your SQL statement. You can make the keys in the public static $defaults into the same name as the columns you've selected, that way to instantiate your object, you can easily do:

$row = mysqli_fetch_array($result, MYSQLI_ASSOC);
$object = new User($row);

The array_merge takes care of having any extraneous keys that you don't need in the argument they provided. If you need to change options, you can declare them the same way for __construct() with a default array and array_merge to catch arguments and mimic named parameters and defaults values (like in Python)

Community
  • 1
  • 1
q.Then
  • 2,743
  • 1
  • 21
  • 31
0

With Syntactic: https://github.com/topclaudy/php-syntactic

you can just do:

function foo($a = 1, $b = 2, $c = 3, $d = 4){
    return $a * $b * $c * $d;
}

And call it with the arguments you want:

//Call with argument b only
echo s('foo')->in('b', 5)->out(); //Outputs 60

//Call with argument a and argument at index/position 1 (b),
echo s('foo')->in('a', 7)->in(1, 5)->out(); //Outputs 420

//Call with argument c only through dynamic method
echo s('foo')->c(9)->out(); //Outputs 72
cjdaniel
  • 241
  • 2
  • 6
  • I don't see how this is much better than associative arrays? There's also no autocomplete for the suggestions you mentioned. – Yahya Uddin Jun 20 '16 at 17:40
  • I think the lack of autocomplete is the only issue with Syntactic (the same applies to assoc arrays). However, the autocomplete support for the original function is still available in IDEs. This is not possible with assoc arrays. Syntactic support REAL parameter name instead of an array as a container for parameters. The developer has peace in mind when defining functions and methods with default parameters. The default values are defined in the signature of the function or method. No complicated checks (array_merge, null checks). The parameters of your functions remain documentable. – cjdaniel Jun 21 '16 at 15:30
0

If U have that much parameters I'd think about creating an object that you'll pass to class instead of n parameters and every parameter is one field there. In constructor you put required parameters and this is then clean solution.