When blog posts here slow down as they have over the last few weeks, it’s a good bet I have my head down building something.
I ran across an issue today that I’d worked around in the past, but had never explicitly investigated. It appears that when assigning a variable within a multiple condition check in PHP, if you don’t explicitly look for a value the first item in the expression is cast as a bool.
I particularly use this technique a lot in CodeIgniter, for example when pulling data from $this->input->post('foo')
– a method that will either return semi-sanitized data from $_POST['foo']
or will return false
.
I tested this in PHP 5.3 (built-in version on Snow Leopard).
Here is some sample code that demonstrates this issue, and the workaround.
function foo() { return 'foo'; } function bar() { return 'bar'; } if ($foo = foo()) { echo '<p>Condition <code>$foo = foo()</code> passed</p>'; } var_dump($foo); echo '<hr />'; if ($foo = foo() && $bar = bar()) { echo '<p>Condition <code>$foo = foo() && $bar = bar()</code> passed</p>'; } var_dump($foo); echo '<hr />'; if ($foo = foo() && $bar = bar()) { echo '<p>Condition <code>$foo = foo() && $bar = bar()</code> passed</p>'; } echo '<p>$foo:<br />'; var_dump($foo); echo '<p>$bar:<br />'; var_dump($bar); echo '<hr />'; if (false !== ($foo = foo()) && $bar = bar()) { echo '<p>Condition <code>false !== ($foo = foo()) && $bar = bar()</code> passed</p>'; } echo '<p>$foo:<br />'; var_dump($foo); echo '<p>$bar:<br />'; var_dump($bar);
Here is the output of this code:
Condition $foo = foo()
passed
string(3) “foo”
Condition $foo = foo() && $bar = bar()
passed
bool(true)
Condition $foo = foo() && $bar = bar()
passed
$foo:
bool(true)
$bar:
string(3) “bar”
Condition false !== ($foo = foo()) && $bar = bar()
passed
$foo:
string(3) “foo”
$bar:
string(3) “bar”
Hopefully this will be useful to someone else when they run across this issue.
Know why PHP works this way? Let me know in the comments.
UPDATE: wrapping each expression in parenthesis works around this. See comments for more detailed explanation.
even inside an if(), order of operations are the exact same as if it’s a normal assignment.
if you take your original condition, and insert in parenthesis for the default order of operations, then:
$foo = foo() && $bar = bar()
which is equivalent to
$foo = (foo() && $bar = bar())
which is equivalent to
$foo = (foo() && ($bar = bar()))
as further experiment, in your original definitions, if you had bar() return a 0 or false, then $foo would have been assigned false as well, even though foo() returned a string. so it’s also not quite the same as assigning properly to foo and then casting it to a bool.
Hope this helps!
Love the blog + your work btw, keep it up!
In your examples, the interpreter starts the assignment at $foo = and doesn’t stop. Your workaround works because of the parenthesis. You’re letting the interpreter know to evaluate that subexpression first. The false !== bit is not needed. The following should suffice.
if (($foo = foo()) && $bar = bar()) {
Adam, thanks for the detailed explanation. TJ, yep – I discovered that as well and am using that approach.
I just generally as a matter of habit these days wrap all compound assignment expressions and most complex statements in if statements in ()s in the order I want, there is no chance of the parser misinterpreting my wishes then.
I also think that:
if (($foo = foo()) && ($bar = bar())) {
Looks more right.
Plus later what if you add $baz=baz()?
Well, now you have to go back and add those ()’s around bar anyway and if you forget, thats another 10 minutes (at least!) wasted debugging.
Same thing with the maths. Its PEMDAS so if you want to be 100% sure $MATHLIBRARY does what you want, just P everything and forget it.
Yeah, I’ve run into that a few times myself, with the same results – just need to wrap stuff in parenthesis to prevent a bad value from coming in.
It doesn’t even have to be two assignments, doing “if ($foo = foo() && count > 5)” will cause the same problem.