I have a few "setter" methods across classes, and for convenience I've added an optional parameter $previous, which takes an argument by reference and populates it with the existing value before replacing it with the new one. For example:
public function set_value($key, $value, &$previous = null)
{
$previous = $this->get_value($key);
$this->_values[$key] = $value;
return $this;
}
This works fine; however in some circumstances, the corresponding "getter" method is a bit process intensive, and running it unconditionally is a waste. I figured I could test:
if(null !== $previous)
{
$previous = $this->get_value($key);
}
This doesn't work though, as often the variable passed as the argument for $previous hasn't been previously defined in it's scope, and defaults to null anyway. The only solution I've hacked out is:
public function set_value($key, $value, &$previous = null)
{
$args = func_get_args();
if(isset($args[2])
{
$previous = $this->get_value($key);
}
$this->_values[$key] = $value;
return $this;
}
Or, to one-line it:
if(array_key_exists(2, func_get_args()))
{
// ...
}
I don't like the method body being reliant on the argument indices (when it seems it should be unnecessary) Is there a cleaner way to achieve what I'm after here?
I've tried:
if(isset($previous)){}
if(!empty($previous)){}
if(null !== $previous){}
Neither work.
Possible solutions thus far:
if(func_num_args() == $num_params){}
if(array_key_exists($param_index, func_get_args())){}
// 5.4
if(isset(func_get_args()[$param_index])){}
// 5.4
if(func_num_args() == (new \ReflectionMethod(__CLASS__, __FUNCTION__))
->getNumberOfParameters()){}
#DaveRandom -- So, something in the area of:
define('_NOPARAM', '_NOPARAM' . hash('sha4096', microtime()));
function foo($bar = _NOPARAM)
{
// ...
}
#hoppa -- Use case:
$obj->set_something('some_key', $some_value, $previous) // set
->do_something_that_uses_some_key()
->set_something('some_key', $previous) // and reset
->do_something_that_uses_some_key()
-> ...
Instead of:
$previous = $obj->get_something('some_key'); // get
$obj->set_something('some_key', $some_value) // set
->do_something_that_uses_some_key();
->set_something($previous) // and reset
->do_something_that_uses_some_key();
-> ...
possibly not how you wanted to solve your problem (testing somehow optional arguments), but this is how I would implement it:
public function set_value($key, $value)
{
$this->_values[$key] = $value;
return $this;
}
public function set_get_value($key, $value, &$previous)
{
$previous = $this->get_value($key);
$this->_values[$key] = $value;
return $this;
}
Use case example:
$obj->set_get_something('some_key', $some_value, $previous) // set AND get
->do_something_that_uses_some_key()
->set_something('some_key', $previous) // and reset
->do_something_that_uses_some_key()
-> ...
Why use another function?
This solution has a few advantages:
the name is more explicit, less confusion for other coders
no hidden side effects
solves your problem with (undefined) variables already having a value
no overhead of calling func_num_args, or some other "meta" function
EDIT: typo in code.
EDIT 2: removed default value of &$previous set_get_value() function (thanks to draevor)
Extracted from the comments / discussion above:
In order to check whether the argument was passed you have 2 options - check the value of the argument against a value (as you've done with null) or check the number of arguments.
If you go with the first option, there's no value that cannot be passed from outside the function, so there will always be a chance for false positives (the same thing that's happening now with null). DaveRandom's example with a random string should be enough for most cases though, but I see it as overkill.
I think the second option is the most clean (fast, readable, etc). As a small improvement over what you've already done with func_get_args, I'd use func_num_args - this way you'll be checking the number of passed arguments, not the argument indices.
Related
My current way:
class A {
public function function_b($myint) {
if (!is_numeric($myint)) return false;
// code ...
}
}
I would like to abandon the function is_numeric() like this:
public function function_b(Integer $myint) {
// code ...
}
It works with arrays like this:
public function function_c(Array $arr) {
// only executes following code if $arr is an array / instance of Array!
}
Note: the function has to return false if the value isn't a number (int)! I don't want to cast it.
How would you short my current code? Thanks in advance!
You can't force strict types in function prototypes in PHP inherently, because it's not a strictly typed language. PHP is a weakly typed language and trying to go against the grain will only hurt you in many situations. Also, is_numeric does not guarantee that your value is of type int (for what it's worth).
What you can do is analyze your need for why you think this approach is necessary in the first place and decide on how to best implement this without creating potential for bugs.
For example, take the following scenario where what your method expects is an ID for a database query.
class MyClass {
public function getUser($id) {
if (!is_int($id)) {
throw new Exception("Invalid argument supplied. Expecting (int), but argument is of type (" . gettype($id) . ").");
}
// Otherwise continue
$db = new PDO($dsn);
$stmt = $db->prepare("SELECT username FROM users WHERE user_id = ?");
$stmt->execute(array($id));
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $result;
}
}
$MyObject = new MyClass;
$result = $MyObject->getUser($_POST['id']);
/* The problem here is $_POST will always be of type string. */
What this should tell you is that it makes no sense to force type checking here since PHP will have done the right thing for you had you just let it alone.
The question you need to be asking yourself is not "How do I force strict typing?", but rather "Why would I need to force strict typing at all?".
You should look into typecasting:
http://php.net/manual/en/language.types.type-juggling.php#language.types.typecasting
Just use (int) when accessing the value to typecast it to an integer.
You could just typecast it:
public function function_b($myint) {
$myint = (int) $myint;
}
Or better yet add a public setter to class A which will do it for you every time you set the value:
class A
{
public function setMyInt($myInt)
{
$this->myInt = (int) $myInt;
}
}
-- Update (based on comment) --
class A
{
public function doSomethingWithAnArray(array $array)
{
....
}
}
Notice the keyword array in the signature of the doSomethingWithAnArray method, now if you don't pass an array to this function PHP will throw a fatal error and cease code execution. This is known as typehinting, and can be applied to objects as well.
function needsInteger($int) {
if (((int) $int) != $int) return false;
// ...
}
The advantage here is that you can still accept loosely typed parameters, but the non-strict equality check against the cast value will yield an acceptable result.
I've asked a question before, that essentially took the $null = null approach as a given, to returning a null reference in PHP.
After some cursory Googling, I didn't turn up much; leaving me to assume that the aforementioned approach is the best (read, only) way. However, it seems odd to me that PHP would (still) fail to support such functionality.
Anyways, if it's unclear; what (other, if any) ways exist to return null from a function by reference in PHP? I'm asking specifically about returning the null reference, not about the ternary operator issue that surfaced to explain my linked question.
For instance:
function &return_null(){
return null;
}
$null_ref = return_null(); // fails
However:
function &return_null(){
$null = null;
return $null;
}
$null_ref = return_null(); // succeeds
I'm asking because I'm a bit OCD when creating reusable libraries; I really like clean code, with respect to however clean it can get in a given language. Using a placeholder $null = null makes my skin crawl, despite it achieving the desired functionality.
For the sake of completeness #yes123, here's the method snippet where this problem lives:
public static function &getByPath(Array &$array, $path, $delimiter){
if(!is_array($path)){
$path = explode($delimiter, $path);
}
$null = null;
while(!empty($path)){
$key = array_shift($path);
if(!isset($array[$key])){
return $null;
}
if(!empty($path) && !is_array($array[$key])){
return $null;
}
$array = &$array[$key];
}
return $array;
}
There's also setByPath(), issetByPath(), and unsetByPath() in this ArrayPath class. I've aliased these static methods with the overloading magic. When an instance is constructed, an array is passed to the constructor (along with a delimiter), and the magic methods call the static ones using the referenced array of the instance. It's working pretty swell so far. In addition, I've written an alias function, array_path() that simply returns an instance. So for example, one can do:
$array = array(
'foo' => array(
'bar' => array(
'hello' => 'world',
),
),
);
array_path($array, '/')->{'foo/bar/hello'} = 'universe';
var_dump($array);
/*
array(1) {
["foo"]=>
array(1) {
["bar"]=>
array(1) {
["hello"]=>
string(8) "universe"
}
}
}
*/
I'm also a bit anal about my code. There is no functional difference here but I think this looks and reads better. But that is just my personal preference.
function &getByPath(array &$array, $path, $delimiter = '/'){
$result = NULL;
// do work here and assign as ref to $result if we found something to return
// if nothing is found that can be returned we will be returning a reference to a variable containing the value NULL
return $result;
}
I'm not sure if "references" and "clean code" go together... :(
Anyway, references are not "pointers to" objects/values, rather, they are "pointers to" variables. Thus, only a variable is a suitable target. Said variable can "name" an object/value (read: be assigned a value), as demonstrated in the post. The post, however, does not return a "null reference" -- it returns a reference to a variable that "names" null.
(And then people wonder why I reject the terminology that a variable "stores a reference to an object" when dealing with high-level languages/concepts...)
Happy coding.
As for returning by reference, it will not work the other way
You can only return variables by reference from a function - nothing else.
http://www.php.net/manual/en/language.references.return.php
You may want to rethink if reference is something you really need, especially that you are already passing $array as reference and returning it
A solution for your particular problem is to generalize your code:
function &getByPath(array &$array, $path, $delimiter = '/'){
if (!is_array($path)){
$path = explode($delimiter, $path);
}
$current =& $array;
foreach ($path as $part) {
$current =& $current[$part];
}
return $current;
}
Now no magic null values are returned. Instead the function will return the element as the path specified, even if it did not yet exist (the path will be added to the array and initialized with null).
$element =& getByPath($array, 'hallo/world');
isset($element); // if the element didn't exist, this will return false
$element = 'hi'; // we can set the element, even if it did not exist
Oh, and by the way: There is no other way to return null by reference and I also don't see why you have a problem with that ;) Returning by reference means returning a variable and, well, null aint one.
I just do this (without initialising $null):
return $null;
It has the benefit of being like NikiC mentioned, where you can simply use isset($result) to determine if a result exists.
I have identified an issue that always produce bugs in my application. It is that PHP is generally quite lax about passing null or empty variables to a function. For example
function do_this($a, $b, $c) {
....
}
One error-prone call could be
do_this($request['a'], $request['b'], $request['c']);
As PHP just silently passes null if any of the keys is not found. I have tried use doing error checking inside the function, as below:
function do_this($a, $b, $c) {
if (empty($a)) throw new Exception('$a is not defined!');
if (empty($b)) throw new Exception('$b is not defined!');
if (empty($c)) throw new Exception('$c is not defined!');
}
It's a headache when the function takes many parameters.
I could use E_STRICT, but I am using many third-party plugins and working off Wordpress, so I'll be getting warnings from other packages.
What's a good way to validate many parameters of a function call in PHP?
First off. you shouldn't be passing values without checking your indices like that. That's very very bad.
As for your question, why not just define default values and then loop through your variables to print an error?
function do_this($a = null, $b = null, $c = null) {
$numargs = func_num_args();
$arg_list = func_get_args();
for ($i = 0; $i < $numargs; $i++) {
if ($arg_list[$i] == null) {
//fail
}
}
}
You don't need all that code of course, just an example.
See here for more info : http://www.php.net/manual/en/function.func-get-args.php
You don't need to use empty inside your function, since the variables are certainly set. if (!$a) will do.
It's always possible to pass incorrect values, regardless of whether this happens due to variables not being set in the scope calling the function or just because the values are bad. At some point you need to check anyway. See PHP function param type best practices.
You must check in the scope calling the function for non-existing variables, not within the function. I.e.:
if (isset($foo, $bar, $baz)) {
do_this($foo, $bar, $baz);
}
You must always do this if there's a legitimate chance the variables may not exist, this is not specific to passing them as parameters to functions.
Have a look at args module from NSPL. It makes argument validation an easy process. To check all arguments in the function from your example you just have to add only one line of code:
function do_this($a, $b, $c)
{
expectsAll([nonEmpty, int], [$a, $b, $c]);
// do this...
}
More examples here.
Why php's string is a value type? It is copied all over the place every time the argument is passed to a function, every time the assignment is made, every concatenation causes string to be copied. My .NET experience tells me that it seems inefficient and forces me to use references almost everywhere. Consider the following alternatives:
Alternative 1
// This implementation hurts performance
class X {
public $str;
function __construct($str) { // string copied during argument pass
$this->$str = $str; // string copied here during assignment
}
}
Alternative 2
// This implementation hurts security
class Y {
public $str;
function __construct(&$str) {
$this->$str = &$str;
}
}
// because
$var = 'var';
$y = new Y($var);
$var[0] = 'Y';
echo $y->var; // shows 'Yar'
Alternative 3
// This implementation is a potential solution, the callee decides
// whether to pass the argument by reference or by value, but
// unfortunately it is considered 'deprecated'
class Z {
public $str;
function __construct($str) {
$this->$str = &$str;
}
}
// but
$var = 'var';
$z = new Z(&$var); // warning jumps out here
$var[0] = 'Z';
echo $y->var; // shows 'Zar'
The question: What pain should I choose Performance / Security / Deprecation
PHP handle's it's variables pretty reasonably. Internally, PHP uses a copy-on-modification system.
That is to say that those values will be assigned by reference until one of them is changed, in which case it will get a new slot in memory for the new value.
Passing vars by reference is going to hurt performance.
Your example #1 is the best performance and best way to go about it.
class X {
public $str;
function __construct($str) {
$this->str = $str;
}
}
When passing a non-existent value by reference, PHP creates the value and sets it to NULL. I noticed it when memory increases were occurring while checking empty values in some functions. Take the following function:
function v(&$v,$d=NULL){return isset($v)?$v:$d;}
$bar = v($foo, $default);
This would be shorthand for:
if(isset($foo))
{
$bar = $foo;
}
else
{
$bar = $default;
}
However, when passing non-existent variables PHP creates them. In the case of variables - they are removed as soon as the method/function ends - but for checking super global arrays like $_GET or $_POST the array element is never removed causing extra memory usage.
$request_with = v($_SERVER['HTTP_X_REQUESTED_WITH']);
Can anyone explain why this happens and if it is a PHP todo fix or a feature for some other crazy use of values?
XeonCross' function v is a shorthand for the often used:
$val= isset($arr['elm']) ? $arr['elm'] : 'default'
to avoid the dreaded 'Undefined index: elm' notice. A nice helper function would be:
function ifset(&$v1, $v2 = null) {
return isset($v1) ? $v1 : $v2;
}
as Xeoncross suggested, so you could write the much nicer
$val = ifset($arr['elm'],'default')
however, this has a lot of interesting (?) quirks in our beloved "language" that we call PHP:
inside the function ifset, $v1 seems UNSET, so it correctly returns the value $v2 and you might conclude that ifset works ok. But afterwards $arr['elm'] is silently set to NULL. So consider the following:
function wtf(&$v) {
if (isset($v))
echo "It is set";
else
echo "It is NOT set";
}
$p=[];
wtf($p['notexist']); => It is NOT set
$p; => [ 'notexist' => NULL ]
But this is another delusion, as the isset() function returns false for NULL values as well:
$x=NULL;
isset($x) => false... huh??
Did we expect this? well.. it is in the documentation, so this is by design as well. Welcome to the wonderful world of php.
The reason you have the memory leak, is because you're telling it to.
When you ask for a reference parameter, PHP will provide you with one. When you are calling a function with an unset variable, PHP will set the variable and then pass the reference to that new variable. When you call it with a superglobal, it creates the missing index. That's because you told it to.
However, I must ask why specifically do you need variable references? 99.9% of the time you don't really need them. I suspect that it'll work just fine to do:
function v($v, $d = null) { return isset($v) ? $v : $d; }
Or, if you really must use references (which you can't get around your original problem with), you should also return a reference:
function &v(&$v, $d = null) {
if (isset($v)) {
return $v;
}
return $d;
}
Otherwise it's pointless to take a reference and not return one...