As designed <xf:set casting

digitalpoint

Well-known member
Affected version
2.2.5
Not entirely sure this is a bug, but it sure gave me some unexpected results. Has to do with a normal comparison operator not casting the variable object to a string, so it compares as "1" (true that it's an object). If you force it to a string with something like trim(), it gives what I would consider the expected results.

Running this test in a template:
HTML:
<xf:set var="$test">2</xf:set>

<xf:if is="$test == 1">test == 1<br /></xf:if>
<xf:if is="$test == 2">test == 2<br /></xf:if>
<xf:if is="trim($test) == 1">trim(test) == 1<br /></xf:if>
<xf:if is="trim($test) == 2">trim(test) == 2<br /></xf:if>

Yields this result:
Code:
test == 1
trim(test) == 2
 
The value of $test here isn't a string or an int but rather a string wrapped in a XF\PreEscaped object, so what's happening behind the scenes is you're comparing an object to an integer e.g. $test == 2 and PHP is throwing an E_NOTICE behind the scenes. We silence those but, admittedly, I'm not sure why that results in that value becoming truthy...

The unsurprising thing is the usage of trim which would hit the object's __toString method and therefore compare correctly.

One fix would be to just compare the value as a string:
Code:
<xf:if is="$test == '2'">

But one notable difference between XF1 and XF2 that may be useful here is the changes we made to the xf:set tag. If you want to set a non-string value, you can use the value attribute:

Code:
<xf:set var="$test" value="2" />

$test is now an integer, rather than a string/pre-escaped object.

So the expected output would be:

Code:
test == 2
trim(test) == 2

That all said, I'm still confused by the output you were seeing in your first test so while I don't think there's a bug here, I don't want to completely discount that quirk.
 
Comparing an object to values such as 1 and true will return true despite the notice. Comparing them to 2 will return false.

Do not use ==. Even seasoned PHP developers rarely remember all of its caveats:

PHP:
$a = '123';
$b = '0123';

if ($a == $b) {
    echo 'This line will run.';
}

 
The value of $test here isn't a string or an int but rather a string wrapped in a XF\PreEscaped object, so what's happening behind the scenes is you're comparing an object to an integer e.g. $test == 2 and PHP is throwing an E_NOTICE behind the scenes. We silence those but, admittedly, I'm not sure why that results in that value becoming truthy...

The unsurprising thing is the usage of trim which would hit the object's __toString method and therefore compare correctly.

One fix would be to just compare the value as a string:
Code:
<xf:if is="$test == '2'">

But one notable difference between XF1 and XF2 that may be useful here is the changes we made to the xf:set tag. If you want to set a non-string value, you can use the value attribute:

Code:
<xf:set var="$test" value="2" />

$test is now an integer, rather than a string/pre-escaped object.

So the expected output would be:

Code:
test == 2
trim(test) == 2

That all said, I'm still confused by the output you were seeing in your first test so while I don't think there's a bug here, I don't want to completely discount that quirk.
Ya, I wasn't surprised that trim() caused the correct comparison (it's why I did it because I knew it would cast the XF\PreEscaped object to a string)... that didn't come about because I was randomly trying every possible function. hah

I understand why it happens, was just thinking it might cause issues for people, that's all. The <xf:set var="$test" value="2" /> thing is great news. :)

Comparing an object to values such as 1 and true will return true despite the notice. Comparing them to 2 will return false.

Do not use ==. Even seasoned PHP developers rarely remember all of its caveats:

PHP:
$a = '123';
$b = '0123';

if ($a == $b) {
    echo 'This line will run.';
}

Yep, my example was a dumbed down version of what I was actually trying to do, which required comparison of the set var to an integer (which is why I didn't use ===.

That being said, the <xf:set var="$test" value="2" /> thing that forces it to an integer is news to me and solves my issue without doing something ugly like casting the object to a string with trim().
 
I've often used <xf:set var="$test" value="{{ 2 }}" /> so it is clear that value is being assigned the result of a computation rather than what might be a raw string-like thing
 
Just to be clear, <xf:set var="$test" value="2" /> will actually set the variable to the string "2". If you really need an actual integer type, @Xon's comment is correct. (Similarly, you'd want to do something like {{ true }} if you needed a boolean.)

The <xf:set var="$test">...</xf:set> syntax is really designed for setting HTML blocks that you may want to reuse or for template readability purposes. Another example would be for passing HTML into a macro. Hence, we also use the PreEscaped object to avoid having to use a raw filter. There's actually a security benefit here as there are cases where using a raw filter can cause security issues if you end up passing the value in via a different approach that hasn't been escaped as you assumed. This allows you to opt into the "unsafe" behavior in places you know you've handled it. (Admittedly, we're not totally consistent with this approach.)

As such, I think we'll just call this as designed. The PHP object comparison quirks aren't something we'd make any changes for.
 
Back
Top Bottom