I'm wrapping a model function in Yii 1.8 that has the signature:
public save($runValidation=true, array $attributes=NULL)
With a function:
public xSave(array $params)
That allows the addition of a flag and optional message that causes the wrapper function to throw an Exception in the case that the delegated save() function returns false.
I toyed with the idea of overwriting save() with:
public save(
$runValidation=true,
array $attributes=NULL,
$exception_on_error=false,
$exception_message=false
)
but would like to allow the specification of the last two parameters independently from the first and like the idea of allowing the extra readability of passing in an array with string keys.
I have so far:
/**
* Enhanced save function.
* Delegates to standard model save to allow exceptions to be thrown
* in the case where the model was not saved.
**/
public function xSave(array $params=array()){
$_params=array(
'run_validation'=>true,
'attributes'=> null,
'exception_on_failure'=>false,
'exception_message'=>false,
);
array_merge($_params, $params);
// Call the save method.
$is_saved=$this->save($_params['run_validation'],$_params['attributes']);
// Throw exception to if flag set and model not saved.
if($_params['exception_on_failure'] && !$is_saved){
// If no exception message was passed in, use the default.
if($_params['exception_message'] === false){
throw new CException('
Could not '.($this->isNewRecord()?'create':'update').' '.get_class($this).';
Errors: '.CJSON::encode($this->errors)
);
}
// Else throw using the one provided.
throw new CException($exception_message);
}
// Return result of standard save method.
return $is_saved;
}
Firstly I'd like to know if this is a sensible choice, as I may well use it for other parts of the system. I'm currently not too worried about the typing of the parameters although I agree this could be an issue in the future.
Secondly I would also like the ability to throw an exception in the case that $params has a key that is not defined in $_params with a message specifying that key, and would like to do this as part of the array_merge if possible.
To 1), yes, passing arrays is the usual lame workaround in languages that don't support named arguments (see jQuery etc). With the new array syntax, this is even almost readable:
$some->save([
$runValidation => true,
$attributes => ['foo', 'bar']
]);
Inside a function, you can use extract to avoid ugly $params[foobar] references.
For better taste though, persuade #NikiC to get this patch ready ))
To 2), if you plan to use argument arrays systematically, consider a helper function like this:
function parseArgs($args, $defaults) {
foreach($args as $k => $v) {
if(!array_key_exists($k, $defaults))
throw new Exception("invalid argument: $k");
// might want to add some type checking, like
// if(gettype($v) != gettype($defaults[$k])) bang!
}
return $args + $defaults;
}
Usage:
public function xSave(array $params=array()){
extract(parseArgs($params, [
'run_validation'=>true,
'attributes'=> null,
'exception_on_failure'=>false,
'exception_message'=>false,
]));
if ($run_validation)
etc....
The decision to either use single parameters or parameter-arrays is opinion based. It depends on the situation. At least I would keep the design consistent across the whole project.
To decide if there had been unknown parameters passed, you can use array_diff():
$a = array(
'test' => 'foo',
'name' => 'bar'
);
$b = array(
'test' => 'foo',
'name' => 'bar',
'abcd' => '123'
);
$d = array_diff(
array_keys($b), array_keys($a)
);
echo "The following keys can't be understood: " . implode(', ', $d) . PHP_EOL;
However, I would skip that check as it will not "harm" if there are unknown parameters.
Related
I call an object that returns an array given certain chained methods:
Songs::duration('>', 2)->artist('Unknown')->genre('Metal')->stars(5)->getAllAsArray();
The problem lies that every time I want to get this array, for example, in another script, I have to chain everything again. Now imagine that in over 10 scripts.
Is there a way to recall the chained methods for later use?
Since you can't cache the result, you could cache the structure of the call chain in an array.
$chain = [
'duration' => ['>', 2],
'artist' => 'Unknown',
'genre' => 'Metal',
'stars' => 5,
'getAllAsArray' => null
];
You could use that with a function that emulates the chained call using the cached array:
function callChain($object, $chain) {
foreach ($chain as $method => $params) {
$params = is_array($params) ? $params : (array) $params;
$object = call_user_func_array([$object, $method], $params);
}
return $object;
}
$result = callChain('Songs', $chain);
If you can not cache your results as suggested, as I commented, here are a couple ideas. If your application allows for mixing of functions (as in you are permitted by standards of your company's development rules) and classes, you can use a function wrapper:
// The function can be as complex as you want
// You can make '>', 2 args too if they are going to be different all the time
function getArtists($array)
{
return \Songs::duration('>', 2)->artist($array[0])->genre($array[1])->stars($array[2])->getAllAsArray();
}
print_r(getArtists(array('Unkown','Metal',5)));
If you are only allowed to use classes and __callStatic() is not forbidden in your development and is also available in the version of PHP you are using, you might try that:
// If you have access to the Songs class
public __callStatic($name,$args=false)
{
// This should explode your method name
// so you have two important elements of your chain
// Unknown_Metal() should produce "Unknown" and "Metal" as key 0 and 1
$settings = explode("_",$name);
// Args should be in an array, so if you have 1 value, should be in key 0
$stars = (isset($args[0]))? $args[0] : 5;
// return the contents
return self::duration('>', 2)->artist($settings[0])->genre($settings[1])->stars($stars)->getAllAsArray();
}
This should return the same as your chain:
print_r(\Songs::Unknown_Metal(5));
It should be noted that overloading is hard to follow because there is no concrete method called Unknown_Metal so it's harder to debug. Also note I have not tested this particular set-up out locally, but I have notated what should happen where.
If those are not allowed, I would then make a method to shorten that chain:
public function getArtists($array)
{
// Note, '>', 2 can be args too, I just didn't add them
return self::duration('>', 2)->artist($array[0])->genre($array[1])->stars($array[2])->getAllAsArray();
}
print_r(\Songs::getArtists(array('Unkown','Metal',5)));
I wrote a lib doing exactly what you're looking for, implementing the principle suggested by Don't Panic in a high quality way: https://packagist.org/packages/jclaveau/php-deferred-callchain
In your case you would code
$search = DeferredCallChain::new_(Songs::class) // or shorter: later(Songs::class)
->duration('>',2) // static syntax "::" cannot handle chaining sadly
->artist('Unknown')
->genre('Metal')
->stars(5)
->getAllAsArray();
print_r( $search($myFirstDBSongs) );
print_r( $search($mySecondDBSongs) );
Hoping it will match your needs!
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(?)).
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.
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
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)
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
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.
I have a model that runs a query with a bunch of conditions in the SQL. As a result, the model needs to accept a lot of parameters, i.e:
this->model_name->method($param1, $param2, ... )
On the model side, I typically set this up as
function method($param1 = NULL, $param2 = NULL, ... )
Each of those parameters is optional, and use cases will vary around the app. So my question is: at what point (if ever) does it make sense to start passing these parameters to the method via an array, a la:
$params = [
'param1' => 'whatever',
'param2' => 'whatever',
...
]
this->model_name->method($params)
With the end goal being, I suppose, cleaner code, and less instances of method(null, null, null, null, $param) unless that's an okay thing to do.
Most answers have been supportive of the array method (which, generally speaking, I would also agree with), but I'll play devil's advocate and list some negatives:
Documentation is less clear
Most methods of documenting functions/methods will list the parameters of that function individually. For example, a function with a basic DocBlock will look like this:
/**
* A function that accepts an array of params
* #param array $param_array An array of key=>value arguments
*/
function accept_array($param_array = array('key1' => 'first_val', 'key2' => 'second_val')) {
var_dump($param_array);
}
Note how the DocBlock doesn't directly support individual parts of the $param_array, just the array as a whole. In contrast, listing all the arguments individually looks like this:
/**
* A function that 'normal' params
* #param string $key1 First argument
* #param string $key2 Second argument
*/
function accept_normal($key1 = 'first_val', $key2 = 'second_val') {
echo $key1;
echo $key2;
}
This is also a problem if you expect your functions to be fairly self-documenting, as in the first example you're not required to actually list your expected arguments in the function itself.
Default values may not work as expected
'As expected' is probably a bit of a loaded phrase (and this is probably one of the more obvious problems), but take the following:
function accept_array($param_array = array('key1' => 'first_val', 'key2' => 'second_val')) {
var_dump($param_array);
}
accept_array(array('key2' => 'a_different_val'));
Some may expect the var_dump to include the default value of key1 and the new value of key2, but the whole array is replaced, meaning you will need to remember to set default values for each key manually in each function, like so:
function accept_array($param_array = array()) {
if (!isset($param_array['key1'])) { $param_array['key1'] = 'first_val'; }
if (!isset($param_array['key2'])) { $param_array['key2'] = 'second_val'; }
var_dump($param_array);
}
accept_array(array('key2' => 'a_different_val'));
No automatic filtering
Listing the arguments the 'normal' way also gives you a built-in set of filters. Take for example this quick and dirty user search:
/**
* We want to allow searching for users by user_id or email only!
* #param array $param_array
*/
function find_user($param_array = array('user_id' => 0, 'email' => '')) {
foreach ($param_array as $field => $value) {
$this->db->or_where($field, $value);
}
$this->db->get('users');
}
find_user(array('first_name' => 'Joe', 'last_name' => 'Bloggs'));
Without manually adding some 'accepted keys' type validation on the $param_array, a call to the find_user() function can essentially use whatever fields it likes. The simpler version would obviously look like this:
/**
* We want to allow searching for users by user_id or email only!
* #param int $user_id
* #param string $email
*/
function find_user($user_id = 0, $email = '') {
$this->db->or_where('user_id', $user_id);
$this->db->or_where('email', $email);
$this->db->get('users');
}
// No way for me to submit any other fields, they'll just fail when they get to the query
find_user('Joe', 'Bloggs'));
I accept some of these are a bit entry-level and there's probably many more that I missed (feel free to comment with more and I'll copy them into the reply with credit), but hopefully there's enough there to make people think twice about automatically using the 'array method' without thinking about manual validation and documentation etc.
Passing an array of parameters provides a better option for self-documenting your code.
When I use many parameters, I often find myself using a style like:
// do_something_model($enable_option1,$enable_option2,$enable_option3)
do_something_model(FALSE, TRUE, FALSE)
where I carry a comment line with the parameter names to remind myself of how I am
using the model.
In such a case, using an array with meaningfully named keys provides a useful mnemonic.
More recently, I am also using more wrapper functions. For example, I may have my
basic model method do get all my data from a table and this method will have a few
options.
I then define a new method that does a specific task and then invoke the basic method within it using the correct options.
Footnote
I find that if my methods have "too many options", it is better to rethink the purpose of the method and to break it up into two or more specialized methods that are easier to use.
I would recommend the array version as well. Symfony2 also uses this pattern a lot, for instance in rendring templates, creating form classes and creating http responses in general. You just have to make sure you cleanly document all possible parameters.
You could go either route, but an array would definitely keep your methods cleaner. It makes perfect sense to pass the parameters as an array.
Can pass a value to specific argument in function ?
function fun1($a,$b)
{
echo $b;
}
#fun1(123);
Functions can be defined so that they do not require all parameters. For example:
function foo($a, $b = 2) {
echo $a + $b;
}
foo(1); //gives 3
Read about default function values here
However, you cannot pass in later parameters without specifying earlier ones. Some simple programming-function-parameters-basics... when you do foo($b) the function has no idea that the variable was named b... It just gets the data; usually a primitive type (in this case an int) or a reference.
You haven't specified how you're using these variables, so you may want to give a dummy value like "-1" to $a (and handle it in your function) (foo(-1, 123)), or rewrite your function so that $a is the second parameter with the default value. (function foo($b, $a = NULL))
That's why you must pass the variables in order; the function assumes you're using it right, and it lines up the values passed with the function definition. function foo($a, $b) means "I'm assuming I should associate your first value with a and your second value with b)".
With your original example function foo($a, $b):
No context, so I would just say do this function foo($b, $a = some_default_value). However, I'm assuming you're using $a and $b equally so you could check to see if it was some default-invalid-value and act on it. However, if your function performs different tasks depending on the (number of) parameters passed, you probably want to separate your function.
If you insist on not switching the order, you could call foo(-1, 123) with a dummy value and check it. Again though, same problem as above
Edit: You've given another example foo($a, $b, $c) and you said you want to do foo($b) to update the middle value. See the explanation in the first paragraph about how a function knows what parameter is what.
If you mean you want to pass an arbitrary set of variables to a function and it knows which ones it got? Again I don't think this is the best practice (you'll need to give us more detail about how you're using this) but you could pass an associative array:
function foo($arr) {
if (isset($arr['a'])) {
echo $a;
}
if (isset($arr['b'])) {
echo $b;
}
if (isset($arr['c'])) {
echo $c;
}
}
foo(array('b' => 123));
I feel horrible after writing this function :P
<?php
function FUN1($a, $b)
{
echo "HI";
echo $b;
} //$_a= 123; //error_reporting (E_ALL ^ E_NOTICE ^ E_WARNING); //$b=23; echo #FUN1(123);//it gives HI123
?>
I formatted your function. Firstly, when I tried that call it doesn't give me "HI123". Secondly, # is bad practice and really slows down the code. Thirdly, you don't echo FUN1 since it doesn't return anything; your function prints the stuff itself.
You (your student) are/is going in the wrong direction. As I said in my comment, functions already have a beautiful way of sorting out the parameters. Instead of trying to do something funky and work around that, just change your approach.
The example above has no real use and I'm sure in actual code you should just write different functions when you're setting different variables. like setA($a) setB($b) setC($c) setAll($a, $b, $c) and use them accordingly. Arrays are useful for easy variable length functions, but if you're checking each tag to do something, then something's wrong.
If you only want to pass one argument, you could make a wrapper function like this:
function PassOne($arg)
{
fun1(NULL,$arg);
}
function fun1($a,$b)
{
echo $b;
}
Forgive any inaccuracies. It's been a while since I coded in PHP.
If you want to ensure the order of arguments, you can pass a single array as an argument.
$args = array(
'name' => 'Robert',
'ID' => 12345,
'isAdmin' => true
);
example($args);
function example($args)
{
echo $args['name']; // prints Robert
echo $args['ID']; // prints 12345
echo $args['isAdmin']; // prints true
}
Using this approach, you can also hard-code default values into the function, replacing them only when they're provided in the argument array. Example:
$args = array(
'name' => 'Robert',
'ID' => 12345
// Note that I didn't specify whether Robert was admin or not
);
example($args);
function example($args)
{
$defaultArgs = array(
'name' => '',
'ID' => -1,
'isAdmin' => false // provides a default value to incomplete requests
);
// Create a new, mutable array that's a copy of the default arguments
$mixArgs = $defaultArgs;
// replace the default arguments with what was provided
foreach($args as $k => $v) {
$mixArgs[$k] = $v;
}
/*
Now that we have put all the arguments we received into $mixArgs,
$mixArgs is mix of supplied values and default values. We can use
this fact to our advantage:
*/
echo $mixArgs['name']; // prints Robert
// if ID is still set to the default value, the user never passed an ID
if ($mixArgs['ID'] == -1) {
die('Critical error! No ID supplied!'); // use your imagination
} else {
echo mixArgs['ID']; // prints 12345
}
echo mixArgs['isAdmin']; // prints false
// ... etc. etc.
}
2018's PHP syntax and defaults
function example($args=[], $dftArgs=['name'=>'', 'ID' => -1, 'isAdmin'=>false])
{
if (is_string($args))
$args = json_decode($args,true); // for microservice interoperability
$args = array_merge($dftArgs,$args);
// ... use $args
}
// PS: $dftArgs as argument is not usual, is only a generalization
No.
But by convention you can skip arguments to built in functions by passing NULL in that position:
fun1(NULL, 123);
Obviously this is doesn't make sense for everything - for example this makes no sense:
$result = strpos(NULL, 'a string');
For user defined functions, it's up to you to handle the arguments in whatever way you see fit - but you might find func_get_arg()/func_get_args() useful for functions that use an indeterminate number of arguments.
Also, don't forget you can make arguments optional by defining default values:
function fun ($arg = 1) {
echo $arg;
}
fun(2); // 2
fun(); // 1
Note that default values can only be defined on the right-most arguments. You cannot give an argument a default value if an argument to its right does not have one. So this is illegal:
function fun ($arg1 = 1, $arg2) {
// Do stuff heere
}
When calling a function is there a way to simplify the argument list? Instead of using $blank.
$subscribe=1;
$database->information($blank,$blank,$blank,$blank,$blank,$blank,$subscribe,$blank,$blank,$blank,$blank,$blank);
function information ($search,$id,$blank,$category,$recent,$comment,$subscribe,$pages,$pending,$profile,$deleted,$reported) {
//code
}
You could pass in an array with the specified keys, and merge it with an array of default values
So instead of
function foo($arg1 = 3, $arg2 = 5, $arg3 = 7) { }
You'd have
function foo($args) {
$defaults = array(
'arg1' => '',
'arg2' => null,
'arg3' => 7
);
// merge passed in array with defaults
$args = array_merge($defaults, $args);
// set variables within local scope
foreach($args as $key => $arg) {
// this is to make sure that only intended arguments are passed
if(isset($defaults[$key])) ${$key} = $arg;
}
// rest of your code
}
Then call it as
foo(array('arg3' => 2));
Yes, pass an array instead, or refactor. A long arguments list is usually a bad smell.
function information(array $params) {....
information(array('search'=>'.....
Twelve arguments are generally too many for one function. It's likely that your code could be simplified (including the argument lists getting shorter) by refactoring function information which looks likely to be a monster.
Stopgap measures you can use in the meantime are
adding default argument values
making the function accept all its arguments as an array
Both of the above will require you to visit all call sites for the function for review and modification.
Adding default arguments is IMHO the poor choice here, as by looking at the example call it seems that you would need to make all arguments default, which in turn means that the compiler will never warn you if you call the function wrongly by mistake.
Converting to an array is more work, but it forces you to rewrite the calls in a way that's not as amenable to accidental errors. The function signature would change to
function information(array $params)
or possibly
function information(array $params = array())
if you want all parameters to be optional. You can supply defaults for the parameters with
function information(array $params) {
$defaults = array('foo' => 'bar', /* ... */);
$params += $defaults; // adds missing values that have defaults to $params;
// does not overwrite existing values
To avoid having to rewrite the function body, you can then use export to pull out these values from the array into the local scope:
export($params); // creates local vars
echo $foo; // will print "bar" unless you have given another value
See all of this in action.
You can make it so the function wil automatically fill the variable with a given value like an empty string:
function information ($subscribe, $search="", $id="", $blank="", $category="", $recent="", $comment="", $pages="", $pending="", $profile="", $deleted="", $reported="") {
//code
}
Yes, there are several ways:
Accept an associative array as a single argument, and pass what you need to that. Throw exceptions if a critical argument is missing.
Place critical arguments at the head of the function definition, and optional ones at the end. Give them a default value so that you don't have to declare them.
Recosinder your function. 12 arguments is much too many for one function. Consider using a class/object, or dividing the work between different functions.
Several ways:
function test($input = "some default value") {
return $input; // returns "some default value"
}
function test($input) {
return $input;
}
test(NULL); // returns NULL
function test() {
foreach(func_get_args() as $arg) {
echo $arg;
}
}
test("one", "two", "three"); // echos: onetwothree