How to really validate an integer in PHP (with tests)

Consider this simple task: you receive a variable from an unknown source (form, database, etc) that must absolutely be a valid integer. It can be a positive or negative integer and be stored in a string or an int, but it must be a real integer number made of digits. This is our checklist:

  • Validates integers only, not floats, strings, arrays or booleans
  • Allows positive and negative integers
  • Doesn’t allow hexadecimal, engineering and other notations
  • Allows strings and ints as input

PHP gives us the following ways to almost-but-not-quite perform this task:

  • is_int or is_integer
  • is_numeric
  • regular expressions
  • ctype_digit
  • filter_var

Let’s say you want to validate that a variable fits the bill. What do you do?

Your first reflex would be to try is_int, also known as is_integer, but you would be wrong. These functions will return false for a string, even if it contains an integer. You could first cast your string to an integer with (int) then apply is_int, but since PHP will cast pretty much anything without throwing an error, your could be an array and it would cast to 0 or 1 anyway.

Fine. That makes sense. Now what about is_numeric? 1.3, +1234e44 and 0x539 would all pass the test, so this function won’t work either.

That leaves us with three options: ctype_digit, filter_var and regular expressions. Using regular expressions would be slow, unreadable and overkill for such a simple task, so let’s take a look at ctype_digit. ctype_digit is equivalent to preg_match('/^[0-9]+$/',$string), which is great except that it doesn’t allow negative values, and it only works with strings.

That’s not quite readable, so let’s try our last option, filter_var. It seems that filter_var is the closest match, but it’s not perfect since it allows +123, but blocks -0 (fixed in PHP 5.4.11), 0123 and 000. For most scenarios, it appears that filter_var is still the best function to use.

If these drawbacks are okay to you, this is the code snippet you should use:

if(filter_var($value, FILTER_VALIDATE_INT) !== false){
    echo("This is a valid integer");
}

If you also want to allow values such as -0, 0123 and 000, but want to block +123, use ctype_digit like this instead:

if(ctype_digit(ltrim((string)$int, '-'))){
    echo("This is a valid integer");
}

That’s right. This is how you check for an integer in PHP. Simple eh?

This is the quick test I have used on my crummy old test server. As you can see, ctype_digit is the only method that works 100% of the time, although it’s completely unreadable.

<?
    $values = array(
        '-0',
        -0,
        0,
        123,
        -123,
        '123',
        '-123',
        '0123',
        '123 ',
        '0',
        '000',
        '+123',
        '1.23',
        1.23,
        '123e4',
        '0x123',
        'potato',
        'EEBD',
        false,
        null,
    );

    echo("PHP version: ".phpversion()."\n\n");

    foreach($values as $value){
        echo("TRYING WITH ");
        var_dump($value);

        echo("is_int: ");
        var_dump(is_int($value));

        echo("is_numeric: ");
        var_dump(is_numeric($value));

        echo("regex: ");
        var_dump(preg_match('/^\-?[0-9]+$/',$value));

        echo("ctype_digit: ");
        var_dump(ctype_digit(ltrim((string)$value, '-')));

        echo("filter_var: ");
        var_dump(filter_var($value, FILTER_VALIDATE_INT));
        echo("\n");
        echo("\n");
    }
?>

These are the results:

PHP version: 5.3.19

TRYING WITH string(2) "-0"
is_int: bool(false) ✘
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: bool(false) ✘ (fixed in PHP 5.4.11)

TRYING WITH int(0)
is_int: bool(true)
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: int(0)

TRYING WITH int(0)
is_int: bool(true)
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: int(0)

TRYING WITH int(123)
is_int: bool(true)
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: int(123)

TRYING WITH int(-123)
is_int: bool(true)
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: int(-123)

TRYING WITH string(3) "123"
is_int: bool(false) ✘
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: int(123)

TRYING WITH string(4) "-123"
is_int: bool(false) ✘
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: int(-123)

TRYING WITH string(4) "0123"
is_int: bool(false) ✘
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: bool(false) ✘

TRYING WITH string(4) "123 "
is_int: bool(false)
is_numeric: bool(false)
regex: int(0)
ctype_digit: bool(false)
filter_var: int(123) ✘ (depending on your needs)

TRYING WITH string(1) "0"
is_int: bool(false) ✘
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: int(0)

TRYING WITH string(3) "000"
is_int: bool(false) ✘
is_numeric: bool(true)
regex: int(1)
ctype_digit: bool(true)
filter_var: bool(false) ✘

TRYING WITH string(4) "+123"
is_int: bool(false)
is_numeric: bool(true) ✘
regex: int(0)
ctype_digit: bool(false)
filter_var: int(123) ✘

TRYING WITH string(4) "1.23"
is_int: bool(false)
is_numeric: bool(true) ✘
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

TRYING WITH float(1.23)
is_int: bool(false)
is_numeric: bool(true) ✘
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

TRYING WITH string(5) "123e4"
is_int: bool(false)
is_numeric: bool(true) ✘
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

TRYING WITH string(5) "0x123"
is_int: bool(false)
is_numeric: bool(true) ✘
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

TRYING WITH string(6) "potato"
is_int: bool(false)
is_numeric: bool(false)
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

TRYING WITH string(4) "EEBD"
is_int: bool(false)
is_numeric: bool(false)
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

TRYING WITH bool(false)
is_int: bool(false)
is_numeric: bool(false)
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

TRYING WITH NULL
is_int: bool(false)
is_numeric: bool(false)
regex: int(0)
ctype_digit: bool(false)
filter_var: bool(false)

5 comments on “How to really validate an integer in PHP (with tests)

  1. I was inclined to ask “what about if ((var+1-1) == var) and (floor(var) == var))“.. which is about as weird as php’s internal methods ;-)

  2. I am using PHP 5.6.20.

    Scenario: unknown data source.

    “Your first reflex would be to try is_int, also known as is_integer, but you would be wrong. These functions will return false for a string, even if it contains an integer.”

    The purpose of is_int() is to check a primitive to see if it is in fact the data type integer. Thus, even if your data source in unknown, using is_int() is appropriate. It’s how you use it, your logic, that matters. While you setup a basic test and provide results, something else needs to be said here.

    The string “123″ is a string. If your code is robust, you would also be using is_scalar(), or !is_array(), to make sure your value was not an array. You might use is_null(), or !is_null, in some fashion if necessary. There is also FILTER_REQUIRE_SCALAR to help out when using PHP filter functions.

    For strings representing integers, yes, a regular expression in combination with casting can be useful. Thus, this takes care of, octal, hex, floats, and other thing that represent numbers.

    In regards to ctype_digit(), when the input has been determined to be a string, the solution is to set your logic up to check for negative integers first, then all other integers, zero to the max integer for PHP. In other words, ctype_digit() is a good tool, but only on the proper branch of logic (testing zero through max integer), and only after determining the value is not negative.

    However, you have left out a fundamental test in your article–range checking. It is not enough to just get an integer and test it for type. For most scenarios, you need to know if an integer is within the allowed domain. Going strictly by what you wrote in the beginning, zero is not in your valid domain. Integers include negative whole numbers, zero, and positive whole numbers.

    While true, it does take a combination of functions to get at the truth, one has to realize that the modular nature of this is intentional (to account for the many, many combinations and scenarios). Knowing the combinations and scenarios is anyone’s job as a programmer, not PHP’s.

    You wrote a good article, but more can be done to make truly robust (in PHP) by not treating the entire range of integers equally, given the tools at your disposal.

    ****Force scalar (is_scalar, !is_array, FILTER_INPUT_SCALAR)****
    Determine data type of the value.
    If string,
    —-Use regular expression with preg_match() to start the neg int checking logic
    —-Cast to integer if, and when, appropriate

    Use is_int()
    Range check the value.
    Return the value to be used in the application, false, or throw an exception.

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax