Character checks7 min readExamples run on PHP 8.5.10

ctype_digit()

Learn how PHP 8.4 ctype_digit() tests whether every character in a string is a decimal digit, including its empty-string, sign and integer-argument pitfalls.

Signature

ctype_digit(mixed $text): bool
Available since PHP 4.0.47.48.08.18.28.38.48.5All versions →

Purpose

ctype_digit() answers one narrow question: does every character in a string fall in the range 0 to 9? It is a quick character-class test, handy for validating things like numeric IDs, PIN codes, postal codes in some countries, or the digit-only part of a token before you convert it with (int).

It does not parse numbers. It does not know about signs, decimal points, exponents or whitespace. If you need to decide whether a value can be treated as a number in a broader sense, look at is_numeric() or filter_var() instead.

Signature

Parameters and defaults

$text is the only parameter and has no default. The function is meant to receive a string, and you should always pass one.

The parameter is typed mixed for historical reasons. Integers are handled in a surprising way: an integer from -128 to 255 is treated as the ASCII code of a single character, with negative values having 256 added. Any other integer is treated as a string of its decimal digits. Because of this, ctype_digit(42) tests the character * (ASCII 42) and gives false, whereas ctype_digit('42') gives true. Since PHP 8.1 passing a non-string argument is deprecated, and the manual says it will eventually be interpreted as a string. Cast explicitly with (string) when you really have an integer, or avoid the situation by validating input as a string from the start.

Return value and failure cases

The function returns a bool:

  • true when the string is non-empty and every character is a decimal digit.
  • false when any character is not a digit.
  • false for an empty string, always. This differs from the intuitive "all zero characters are digits" logic, so it is convenient for validation but worth remembering.

With string input it does not throw exceptions and emits no warnings. Bad input simply produces false.

Example 1: basic checks

This example runs several strings through the function and collects the results as pairs, so the output is easy to read. Note that leading zeros are fine, because '007' is still made only of digits.

Example
<?php
$inputs = ['10002', '1820.20', 'wsl!12', '', '007'];
$result = [];
foreach ($inputs as $input) {
    $result[] = [$input, ctype_digit($input)];
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
[["10002",true],["1820.20",false],["wsl!12",false],["",false],["007",true]]

Example 2: things that look numeric but are not

Signs, decimal points, exponent notation and whitespace are all non-digit characters. That includes a trailing newline, which commonly sneaks in from text input.

Example
<?php
$inputs = ['-5', '+5', ' 5', "5\n", '5.0', '1e3'];
$result = [];
foreach ($inputs as $input) {
    $result[] = [$input, ctype_digit($input)];
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
[["-5",false],["+5",false],[" 5",false],["5\n",false],["5.0",false],["1e3",false]]

Every result is false. If you want to allow surrounding whitespace, call trim() first. If you want to allow a sign, strip it deliberately and check the rest.

Example 3: safe conversion to an integer ID

A typical use is guarding a cast. The check prevents junk from silently becoming 0, and the length limit keeps the value comfortably inside the integer range.

Example
<?php
function parseId(string $value): ?int
{
    if (ctype_digit($value) && strlen($value) <= 9) {
        return (int) $value;
    }
    return null;
}

$values = ['0042', 'abc', '', '12345678901'];
echo json_encode(array_map('parseId', $values), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
[42,null,null,null]

The leading zeros in '0042' are accepted and dropped by the cast, giving 42. The empty string is rejected because ctype_digit('') is false, and the long value is rejected by the length limit.

Edge cases

  • Empty string: always false.
  • Negative numbers: the minus sign is not a digit, so '-1' is false.
  • Decimals: '3.14' is false because of the dot.
  • Leading zeros: accepted. If zeros matter (for example, a ZIP code), keep the value as a string rather than casting it.
  • Non-ASCII digits: the check is a byte-oriented character class test, so do not rely on it to recognise digits from other scripts. For Unicode-aware classification consider IntlChar::isdigit() or a preg_match() pattern with the u modifier, if those tools are available to you.
  • Overflow: a long string of digits passes the test but may not fit in an integer. Check the length or compare as strings before casting.

Common mistakes

  1. Passing an integer. ctype_digit(42) does not mean "is 42 a number". It examines the ASCII character with that code, and the behaviour is deprecated from PHP 8.1. Cast to a string first if the value really is an integer.
  2. Expecting it to validate numbers. Values such as '-12', '1.5' and '1e3' are all numeric to is_numeric() but not to ctype_digit().
  3. Forgetting to trim. Form or file input often has extra spaces or a newline, which will make the check fail.
  4. Assuming a passing check means a safe integer. Very long digit strings pass but overflow when cast.
  5. Treating an empty field as valid. The false result for '' helps here, but make sure your logic does not take a separate path for empty values that skips the check.
  • ctype_alnum() checks for letters and digits.
  • ctype_xdigit() checks for hexadecimal digit characters.
  • is_numeric() accepts a wider range of numeric strings, including signs, decimals and exponents.
  • is_int() and is_string() test the type of a variable rather than its content.
  • IntlChar::isdigit() classifies Unicode code points.

Recap

Use ctype_digit() on strings when you need to know that the input is made only of the characters 0 to 9. It returns false for empty strings, signs, decimal points and whitespace. Do not pass integers, since they are interpreted as character codes (deprecated since PHP 8.1), and combine the check with a length limit before casting to int.

Tested examples

Basic digit checks on strings
<?php
$inputs = ['10002', '1820.20', 'wsl!12', '', '007'];
$result = [];
foreach ($inputs as $input) {
    $result[] = [$input, ctype_digit($input)];
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
[["10002",true],["1820.20",false],["wsl!12",false],["",false],["007",true]]
Numeric-looking strings that are not all digits
<?php
$inputs = ['-5', '+5', ' 5', "5\n", '5.0', '1e3'];
$result = [];
foreach ($inputs as $input) {
    $result[] = [$input, ctype_digit($input)];
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
[["-5",false],["+5",false],[" 5",false],["5\n",false],["5.0",false],["1e3",false]]
Guarding an integer cast
<?php
function parseId(string $value): ?int
{
    if (ctype_digit($value) && strlen($value) <= 9) {
        return (int) $value;
    }
    return null;
}

$values = ['0042', 'abc', '', '12345678901'];
echo json_encode(array_map('parseId', $values), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
[42,null,null,null]