Being new to PHP, and with a lot riding on this "function wrapper", I thought I'd get a few opinions and a little feedback. I'd like to get about five comments, if possible.
Now before you ask, I have many reasons for wanting to wrap other (WordPress) functions, the primary being hassle-free upgrading. It was also important for me to be able to set a custom name for each function definition, hence the $wrap array.
But I digress, does this look acceptable and relatively bulletproof?
function core_oo( $function )
{
$args = array_slice( func_get_args(), 1 );
$wrap = array
(
'comment' => 'the_comment',
'comments' => 'have_comments',
'post' => 'the_post',
'posts' => 'have_posts'
);
return call_user_func_array( $wrap[ $function ], $args );
}
... and the function will be called like...
core_oo( 'post', 'arg1', 'arg2' );
Many thanks!
EDIT:
Per chaos's sugeestion below, is this the right way to declare $wrap as static?
static $wrap = array
( ...
Well, your fundamental aim seems like madness, but with your fundamental aim taken as a given, yes, that function looks like a fine way to accomplish it.
You should declare $wrap as static to ensure that you're not pointlessly regenerating the array every time the function is called, though.
And this:
if(!isset($wrap[$function]))
trigger_error('No underlying function known for ' . $function, E_USER_ERROR);
would probably be smart too.
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.
In PHP, when defining a function what is the maximum number of arguments that you can create? This isn't something I am doing, just a question that popped up when discussing it with a colleague :)
Would this be purely down a memory limitation? effectively unlimited?
There is no limit. And you can always use func_get_args(), func_get_arg() and func_num_args() to avoid writing all the arguments in the function definition.
Arguments to a function are pushed on a stack, after which the function is called which in turn reads the stack and uses those values as parameters.
So as long as the stack isn't full, you can keep adding parameters, but it'll depend on the situation, and at design-time you won't know the stack size.
But I really hope this is pure a technical discussion and you don't need it IRL. ;-)
when defining a function what is the maximum number of arguments that you can create?
I'm not aware of specific limit in the number of arguments as a fixed number. A quick test revealed that I had no problems to define a function with 255 555 arguments. It does take some time to define the function and to execute it. However, it works.
As the number was raised, I was running into a memory limit which could be string limit. You might want to improve the test-case, use a buffer and save the file to disk sequentially and include it then:
$count = 255555;
$code = 'function test(%s) {return 1;}; return test();';
$params = ltrim(implode('=0, $p', range(0, $count)), '0, =').'=0';
echo eval(sprintf($code, $params));
I don't really know what are the limits, but either way, if you create a function with too many arguments your code will not be so easy to read it. In case you like to add many function arguments you can do something like that:
$option = array(
'arg_1' => 'value',
'arg_2' => 'value',
....
'arg_x' => 'value'
);
function function_name($args = "")
{
$defaults = array(
'arg_1' => 'default_value',
'arg_2' => 'default_value'
....
'arg_x' => 'default_value'
);
$arguments = array_merge($defaults, (array)$args);
extract($arguments);
echo $arg_1;
echo $arg_x;
}
in PHP, I'm considering doing something like this:
function foo(){
echo 'bar';
}
$fn = 'foo';
$fn();
It works, but is it considered bad practice?
I have an multidimensional array of elements that each have a corresponding function. I would like to store that function name, and call the corresponding functions for each element when traversing the array.
something like:
function render_el1(){ echo 'et';}
function render_el2(){ echo 'to';}
$elements = array(
'el_1' => array(
'name' => 'Element One'
, 'func' => 'render_el1'
)
, 'el_2' => array(
'name' => 'Element Two'
, 'func' => 'render_el2'
)
);
foreach($elements as $element => $options){
$fn = $options['func'];
echo '<h1>'.$options['name'].'</h1>';
if (function_exists($fn)) {
$fn();
}
}
Any comments to this approach is highly welcome, and I'd also like to know what this method is called in programming terms.
Not sure it is bad practice, but it makes your code hard to understand : to understand your short (5 lines) example, I've had to think :-(
Using call_user_func() and other functions of the same kind could have at least one advantage : looking at the code, one would immediatly understand you are calling a function in a way that's not the one we're generally used to.
You want to register functions into an array in your second example and then call them for what looks like a render process. This is similar to using function pointers in C (or paint event callbacks etc). It is an okay approach if you don't want to/can't use polymorphism (the feature that makes OOP worthwhile).
Your approach is simpler at that stage, but will probably get more bloated if you are adding more sophisticated code.
I happened to be making some changes to a WordPress blog and noticed that they use parse_str (http://php.net/parse_str) for parsing and setting their optional parameters to a function.
I'm wondering if there is an advantage to this over sending an array?
Examples:
With array:
$blahOptions = array(
'option_1' => true,
);
BlahArray($blahOptions);
function BlahArray($options = array()) {
$defaults = array(
'option_1' => false,
'option_2' => 'blah',
);
// this would probably be in a function to be used everywhere
foreach ($defaults as $defaultOption => $defaultValue) {
if (!isset($options[$defaultOption])) $options[$defaultOption] = $defaultValue;
}
}
With parse_str:
$blahOptions = 'option_1=1';
BlahString($blahOptions);
function BlahString($options = '') {
$defaults = array(
'option_1' => false,
'option_2' => 'blah',
);
parse_str($options, $defaults);
$options = $defaults;
}
No. That seems like a ridiculous way to pass functional parameter arguments. I could understand it if you needed to recreate $_GET or $_POST variables or something along those lines, but for parameters to a function? That's code smell right there.
They should be using an array, and then utilizing extract() instead. I've worked with Wordpress before, and my advice is to keep the code at arm's length. It is not an example of model programming.
No. There are more disadvantages than advantages.
When you’re using a single string, you just can pass string values. With an array you can use every PHP data type and every element’s value type is independently of each other.
With parse_str, you can potentially drop in the query string from the URL, and the function will work. If you use an array, and you want to use the query string, you'll have to enumerate everything into an array before calling the function.
I'm not totally convinced it's the best way to go, but I see how it can add a bit of flexibility.