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)

2 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 ;-)

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