Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArrayUtils::inArray should use strict in_array? #5

Open
weierophinney opened this issue Dec 31, 2019 · 10 comments
Open

ArrayUtils::inArray should use strict in_array? #5

weierophinney opened this issue Dec 31, 2019 · 10 comments

Comments

@weierophinney
Copy link
Member

Related: zendframework/zend-form#18

In Zend\Form\View\Helper\FormSelect ArrayUtils::inArray is called with the strict parameter false. This causes an issue with string matching ('1.1' and '1.10' treated equivalent):

<?php
$needle = '1.10';
$haystack = ['1.1'];

assert(in_array($needle, $haystack) === false);
// PHP Warning:  assert(): Assertion failed in <file> on line 5

(3v4l: https://3v4l.org/HKM8Q)

Simply changing FormSelect to use strict=true breaks an existing test which uses integer keys in the value options array.

Since ArrayUtils::inArray uses string casting to work around in_array's wonky non-strict behaviour, shouldn't the call to in_array always have strict=true?

diff --git a/src/ArrayUtils.php b/src/ArrayUtils.php
index 17e3ae3..69b9721 100644
--- a/src/ArrayUtils.php
+++ b/src/ArrayUtils.php
@@ -199,7 +199,7 @@ abstract class ArrayUtils
                 }
             }
         }
-        return in_array($needle, $haystack, $strict);
+        return in_array($needle, $haystack, true);
     }

     /**

I've tested this change here (all tests pass) and against zend-form (where it fixes the reported issue).

What's the protocol for testing changes to packages which may have knock-on effects on other packages? Pull every repo that has a dependency on stdlib, apply the patch, and run the tests?


Originally posted by @adamlundrigan at zendframework/zend-stdlib#41

@weierophinney
Copy link
Member Author

@adamlundrigan would everything be ok if you passed true to $strict?


Originally posted by @Ocramius at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

@Ocramius

Simply changing FormSelect to use strict=true breaks an existing test which uses integer keys in the value options array.

I should have included a link to the source there for context. If I simply change this line to add true for $strict then I get a failure in this test case which uses integer keys for the value options because it's strictly searching for "string 0" in an array that contains "int 0".


Originally posted by @adamlundrigan at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

@weierophinney
Copy link
Member Author

I'll do the logical question. Why not

<?php
$needle = '1.10';
$haystack = ['1.1'];

assert(ArrayUtils::inArray($needle, $haystack, **true**) === false);

Originally posted by @Maks3w at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

Sorry I wrote wrong the example. I've edited


Originally posted by @Maks3w at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

Marco asked same question above: zendframework/zend-stdlib#41 (comment)

It breaks an existing test in FormSelect. The value_options array for Select can contain integer keys, which are converted to string when rendered by FormSelect. With a strict check alone that will fail since the haystack has "int 0" but the form data has "string 0". The only workaround that works for both cases is to pass "false" to ArrayUtils::inArray (so it casts everything to strings) but pass "true" to in_array to get strict matching. That's what the code in my PR does.


Originally posted by @adamlundrigan at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

Sorry I'm not able to understand why ArrayUtils::inArray($needle, $haystack, **true**) fail but ArrayUtils::inArray($needle, $haystack, false) { return in_array($needle, $haystack, **true**); } works.

I've missing something in your answer.


Originally posted by @Maks3w at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

ArrayUtils::inArray will cast everything to string when it's $strict argument is false:

    /**
     * Checks if a value exists in an array.
     *
     * Due to "foo" == 0 === TRUE with in_array when strict = false, an option
     * has been added to prevent this. When $strict = 0/false, the most secure
     * non-strict check is implemented. if $strict = -1, the default in_array
     * non-strict behaviour is used.
     *
     * @param mixed $needle
     * @param array $haystack
     * @param int|bool $strict
     * @return bool
     */
    public static function inArray($needle, array $haystack, $strict = false)
    {
        if (!$strict) {
            if (is_int($needle) || is_float($needle)) {
                $needle = (string) $needle;
            }
            if (is_string($needle)) {
                foreach ($haystack as &$h) {
                    if (is_int($h) || is_float($h)) {
                        $h = (string) $h;
                    }
                }
            }
        }
        return in_array($needle, $haystack, $strict);
    }

We need that, or else FormSelect view helper will blow up if you set value_options with integer keys, like this:

        $element = new SelectElement('foo');
        $element->setValueOptions([
            0 => 'label0',
            1 => 'label1',
        ]);

        // Pretend this value came from POST and we're re-displaying the form
        $element->setValue("0");

That's a valid use case. FormSelect view helper will take that and output something like this:

<!-- snip -->
<option value="0" selected="selected">label0</option>
<option value="1" selected="selected">label1</option>
<!-- / snip -->

The types don't match so ArrayUtils::inArray($n, $h, true) won't work there. ArrayUtils::inArray($n, $h, false) fixes this case by casting everything to string before chucking it into in_array.

However, PHP has another thing we need to work around:

var_dump('1.1' == '1.10');
// bool(true)

https://3v4l.org/QRkkA

Which means that ArrayUtils::inArray($n, $h, false) won't work for this case:

        $element = new SelectElement('foo');
        $element->setValueOptions([
            '1.1' => 'label 1.1',
            '1.10' => 'label 1.10',
            '1.2' => 'label 1.2',
            '1.20' => 'label 1.20',
        ]);

        // Pretend this value came from POST and we're re-displaying the form
        $element->setValue("1.1");

We'll end up with both "1.1" and "1.10" selected when we render to HTML because in PHP's bizarro world "1.1" == "1.10".

Basically, we need a check that's strict on value (so "1.1" != "1.10") but not strict type (so "int 0" == "string 0"), and that's what sending true into in_array's $strict regardless of the value provided to ArrayUtils::inArray's $strict is doing. It's making sure in_array does a strict check but casting everything to string first so types aren't an issue.


Originally posted by @adamlundrigan at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

I understand the issue now.

Well, I see inArray has three responsibilities:

  1. wrap in_array
  2. transform needle
  3. transform haystack.

My (abstract) proposal:

  1. Refactor needle transformation as method.

  2. Refactor haystack transformation as method.

  3. Deprecate inArray in favor of :

    in_array(
      ArrayUtils::needleTransform($needle), 
      ArrayUtils::haystackTransform($haystack),
      $strict
    );

Originally posted by @Maks3w at zendframework/zend-stdlib#41 (comment)

@weierophinney
Copy link
Member Author

I think it could be distilled down to a single generic "recursively cast array keys and non-array values to string" which would cover both the needle and haystack transforms. castKeysAndValuesToString is long but leaves no ambiguity of purpose.

Regardless, it doesn't feel like the right solution. The intention of ArrayUtils::inArray was to work around the wonky behaviour of non-strict in_array, but it's not doing that job completely in it's current state.


Originally posted by @adamlundrigan at zendframework/zend-stdlib#41 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant