MidТеория11 min

Числовые строки

Numeric strings, правила преобразования, сравнения, is_numeric(), filter_var

Числовые строки (Numeric Strings)

Что такое числовая строка

В PHP числовая строка (numeric string) -- это строка, содержимое которой может быть интерпретировано как число. PHP различает три вида таких строк:

Вид Описание Пример
Numeric string Полностью числовая строка '42', '3.14', '1e5'
Leading-numeric string Начинается с числа, но содержит не-числовые символы '42abc', '3.14xyz'
Non-numeric string Не начинается с числа 'abc', '', 'true'
<?php
declare(strict_types=1);

// Numeric strings — fully numeric content
$a = '42';        // Integer numeric string
$b = '3.14';      // Float numeric string
$c = '1e5';       // Scientific notation — numeric string
$d = '0x1A';      // NOT numeric (since PHP 7.0)
$e = '0b1010';    // NOT numeric
$f = '0o17';      // NOT numeric

// Leading-numeric strings
$g = '42abc';     // Leading-numeric — starts with number
$h = '3.14xyz';   // Leading-numeric

// Non-numeric strings
$i = 'abc';       // Non-numeric
$j = '';           // Non-numeric (empty string)
$k = ' ';         // Non-numeric (whitespace only — NOT numeric)

Пробелы (Leading/Trailing Whitespace)

PHP допускает пробелы в начале и конце числовой строки. Строка с пробелами вокруг числа все равно считается числовой.

<?php
declare(strict_types=1);

// Leading whitespace is allowed
var_dump(is_numeric('  42'));      // true
var_dump(is_numeric("\t42"));      // true
var_dump(is_numeric("\n42"));      // true

// Trailing whitespace is allowed
var_dump(is_numeric('42  '));      // true
var_dump(is_numeric("42\n"));     // true

// Both sides
var_dump(is_numeric('  42  '));    // true
var_dump(is_numeric(" 3.14 "));   // true
var_dump(is_numeric(" 1e5 "));    // true

// Whitespace inside — NOT numeric
var_dump(is_numeric('4 2'));       // false
var_dump(is_numeric('3. 14'));     // false

// Only whitespace — NOT numeric
var_dump(is_numeric('   '));       // false
var_dump(is_numeric(''));          // false

Важно: Начальные и конечные пробелы (\t, \n, \r, \v, \f, ) допускаются в числовых строках. Пробелы внутри числа -- нет.

Правила преобразования строки в число

Неявное приведение (Type Juggling)

<?php
// Without strict_types — PHP auto-converts

// Numeric strings → exact number
var_dump('42' + 0);        // int(42)
var_dump('3.14' + 0);      // float(3.14)
var_dump('1e5' + 0);       // float(100000)
var_dump('  42  ' + 0);    // int(42) — whitespace trimmed

// Leading-numeric strings → number + E_WARNING (PHP 8.0+)
var_dump('42abc' + 0);     // int(42) + Warning
var_dump('3.14xyz' + 0);   // float(3.14) + Warning

// Non-numeric strings → 0 + TypeError (PHP 8.0+)
// var_dump('abc' + 0);    // TypeError in PHP 8.0+
// var_dump('' + 0);       // TypeError in PHP 8.0+

Эволюция поведения по версиям PHP

Ситуация PHP 7.x PHP 8.0+
'42abc' + 0 42 + Notice 42 + Warning
'abc' + 0 0 + Warning TypeError
'' + 0 0 (тихо) TypeError
'42' + 0 42 42
<?php
declare(strict_types=1);

// In strict mode — NO implicit conversions for function parameters
function add(int $a, int $b): int
{
    return $a + $b;
}

// add('42', '10');  // TypeError! String given, int expected

// But arithmetic operators still do juggling even with strict_types
$result = '42' + '10';  // int(52) — arithmetic operators always juggle

Ловушка экзамена: declare(strict_types=1) влияет только на вызовы функций и возвращаемые значения. Арифметические операторы (+, -, *, /) всегда выполняют type juggling, даже с strict_types=1!

Явное приведение типов

<?php
declare(strict_types=1);

// (int) cast — truncates, no rounding
var_dump((int) '42');          // int(42)
var_dump((int) '42.9');        // int(42) — truncated!
var_dump((int) '42abc');       // int(42)
var_dump((int) 'abc');         // int(0)
var_dump((int) '');            // int(0)
var_dump((int) '  42  ');     // int(42)

// (float) cast
var_dump((float) '3.14');      // float(3.14)
var_dump((float) '42');        // float(42.0)
var_dump((float) '1e5');       // float(100000.0)
var_dump((float) '3.14abc');   // float(3.14)
var_dump((float) 'abc');       // float(0.0)

// intval() with base
var_dump(intval('0x1A', 16));  // int(26)
var_dump(intval('0b1010', 2)); // int(10)
var_dump(intval('0755', 8));   // int(493)
var_dump(intval('42', 10));    // int(42)

// settype()
$val = '42.5';
settype($val, 'int');
var_dump($val);  // int(42)

Сравнение числовых строк

Оператор == (Loose Comparison)

PHP 8.0 кардинально изменил поведение сравнения строк с числами.

<?php
declare(strict_types=1);

// PHP 8.0+ behavior (sane comparison)
var_dump(0 == 'foo');       // false (was true in PHP 7!)
var_dump(0 == '');          // false (was true in PHP 7!)
var_dump(0 == ' ');         // false
var_dump(0 == 'null');      // false
var_dump(0 == "\n");        // false

// Numeric string comparisons — compared as numbers
var_dump('42' == '42.0');   // true — both numeric, compared as numbers
var_dump('42' == '042');    // true — 42 == 42
var_dump('1e1' == '10');    // true — 10.0 == 10
var_dump('10' == '10.0');   // true

// String vs int — numeric string compared as number
var_dump(42 == '42');       // true
var_dump(42 == '42.0');     // true
var_dump(42 == ' 42 ');     // true — whitespace trimmed

// Non-numeric strings compared as strings
var_dump('foo' == 'foo');   // true
var_dump('foo' == 'bar');   // false

PHP 7 vs PHP 8: Критические изменения

<?php
declare(strict_types=1);

// PHP 7.x: string compared to 0 → string cast to int → 0 == 0 = true
// PHP 8.0: 0 is cast to string "0", then "0" == "foo" = false

// BEFORE PHP 8.0 (dangerous behavior):
// 0 == 'any_string'  → true (string cast to 0)
// '' == null          → true
// 0 == null           → true
// '' == false         → true

// PHP 8.0+ (fixed):
var_dump(0 == 'any_string');  // false — string is not numeric, no conversion
var_dump(0 == '0');           // true  — '0' IS numeric, compared as numbers
var_dump(0 == '');            // false
var_dump('' == null);         // true  — special case, both "empty"
var_dump(0 == null);          // true  — null cast to 0

Оператор === (Strict Comparison)

<?php
declare(strict_types=1);

// Strict comparison — no type juggling
var_dump(42 === '42');       // false — different types
var_dump(42 === 42);         // true
var_dump('42' === '42');     // true
var_dump(0 === false);       // false — int vs bool
var_dump(0 === null);        // false — int vs null
var_dump('' === false);      // false — string vs bool
var_dump('' === null);       // false — string vs null

Правило: Всегда используйте === для сравнений, если не нужно намеренное приведение типов. Это устраняет все неожиданности.

Spaceship Operator <=>

<?php
declare(strict_types=1);

// Returns -1, 0, or 1
var_dump('42' <=> '10');     // int(1) — numeric comparison: 42 > 10
var_dump('42' <=> '42');     // int(0)
var_dump('10' <=> '42');     // int(-1)

// Useful in sorting
$values = ['10', '9', '100', '2'];
usort($values, fn(string $a, string $b): int => $a <=> $b);
// Result: ['2', '9', '10', '100'] — compared as numbers!

// Force string comparison
usort($values, fn(string $a, string $b): int => strcmp($a, $b));
// Result: ['10', '100', '2', '9'] — compared as strings (lexicographic)

Таблица поведения операторов

<?php
declare(strict_types=1);

// Complete behavior table for PHP 8.0+

// Arithmetic operators: always convert to number
var_dump('5' + '3');       // int(8)
var_dump('5.5' + '3');     // float(8.5)
var_dump('5' - '3');       // int(2)
var_dump('5' * '3');       // int(15)
var_dump('5' / '3');       // float(1.666...)
var_dump('5' % '3');       // int(2)
var_dump('5' ** '3');      // int(125)

// String concatenation: always converts to string
var_dump(42 . 'px');       // string(4) "42px"
var_dump(3.14 . '!');      // string(5) "3.14!"

// Bitwise operators: convert to int (for strings)
var_dump('5' & '3');       // string(1) "1" — bitwise AND on chars!
var_dump('5' | '3');       // string(1) "7"
var_dump(5 & 3);           // int(1) — bitwise AND on ints
var_dump(5 | 3);           // int(7)

// Comparison operators: type juggling with == , strict with ===
var_dump('42' == 42);      // true
var_dump('42' === 42);     // false
var_dump('42' != 42);      // false
var_dump('42' !== 42);     // true

is_numeric()

Функция is_numeric() проверяет, является ли значение числом или числовой строкой.

<?php
declare(strict_types=1);

// Integers
var_dump(is_numeric(42));           // true
var_dump(is_numeric(-42));          // true

// Floats
var_dump(is_numeric(3.14));         // true
var_dump(is_numeric(INF));          // true
var_dump(is_numeric(NAN));          // true — NaN is technically float

// Integer strings
var_dump(is_numeric('42'));         // true
var_dump(is_numeric('-42'));        // true
var_dump(is_numeric('+42'));        // true

// Float strings
var_dump(is_numeric('3.14'));       // true
var_dump(is_numeric('.5'));         // true — leading dot allowed
var_dump(is_numeric('1e5'));        // true — scientific notation
var_dump(is_numeric('1E5'));        // true — case insensitive
var_dump(is_numeric('1.5e-3'));     // true

// With whitespace
var_dump(is_numeric(' 42 '));       // true
var_dump(is_numeric("\t42\n"));     // true

// NOT numeric
var_dump(is_numeric(''));           // false
var_dump(is_numeric('abc'));        // false
var_dump(is_numeric('42abc'));      // false — leading-numeric is NOT is_numeric
var_dump(is_numeric('0x1A'));       // false — hex string (since PHP 7.0)
var_dump(is_numeric('0b1010'));     // false — binary string
var_dump(is_numeric('0o17'));       // false — octal string
var_dump(is_numeric('true'));       // false
var_dump(is_numeric(null));         // false
var_dump(is_numeric(true));         // false — bool is not numeric
var_dump(is_numeric([]));           // false

Ловушка экзамена: is_numeric('0x1A') возвращает false начиная с PHP 7.0. В PHP 5.x это было true. Также is_numeric(NAN) возвращает true, потому что NaN -- это float. А вот is_numeric(true) -- false, bool не является numeric.

filter_var() для валидации чисел

filter_var() обеспечивает строгую валидацию, подходящую для пользовательского ввода.

FILTER_VALIDATE_INT

<?php
declare(strict_types=1);

// Basic integer validation
var_dump(filter_var('42', FILTER_VALIDATE_INT));        // int(42)
var_dump(filter_var('-42', FILTER_VALIDATE_INT));       // int(-42)
var_dump(filter_var('+42', FILTER_VALIDATE_INT));       // int(42)
var_dump(filter_var('0', FILTER_VALIDATE_INT));         // int(0)

// Returns false for non-integers
var_dump(filter_var('3.14', FILTER_VALIDATE_INT));      // false — float
var_dump(filter_var('42abc', FILTER_VALIDATE_INT));     // false
var_dump(filter_var('abc', FILTER_VALIDATE_INT));       // false
var_dump(filter_var('', FILTER_VALIDATE_INT));          // false
var_dump(filter_var(' 42 ', FILTER_VALIDATE_INT));      // false — whitespace!

// With range options
$options = [
    'options' => [
        'min_range' => 1,
        'max_range' => 100,
    ],
];
var_dump(filter_var('50', FILTER_VALIDATE_INT, $options));   // int(50)
var_dump(filter_var('150', FILTER_VALIDATE_INT, $options));  // false
var_dump(filter_var('0', FILTER_VALIDATE_INT, $options));    // false

// Allow octal and hex
$flags = ['flags' => FILTER_FLAG_ALLOW_OCTAL | FILTER_FLAG_ALLOW_HEX];
var_dump(filter_var('0x1A', FILTER_VALIDATE_INT, $flags));   // int(26)
var_dump(filter_var('0755', FILTER_VALIDATE_INT, $flags));   // int(493)

FILTER_VALIDATE_FLOAT

<?php
declare(strict_types=1);

// Basic float validation
var_dump(filter_var('3.14', FILTER_VALIDATE_FLOAT));       // float(3.14)
var_dump(filter_var('42', FILTER_VALIDATE_FLOAT));         // float(42.0)
var_dump(filter_var('1e5', FILTER_VALIDATE_FLOAT));        // float(100000.0)
var_dump(filter_var('-3.14', FILTER_VALIDATE_FLOAT));      // float(-3.14)

// Returns false for non-floats
var_dump(filter_var('abc', FILTER_VALIDATE_FLOAT));        // false
var_dump(filter_var('42abc', FILTER_VALIDATE_FLOAT));      // false
var_dump(filter_var('', FILTER_VALIDATE_FLOAT));           // false

// Thousands separator
$options = ['options' => ['decimal' => '.'], 'flags' => FILTER_FLAG_ALLOW_THOUSAND];
var_dump(filter_var('1,234.56', FILTER_VALIDATE_FLOAT, $options));  // float(1234.56)
var_dump(filter_var('1,234,567', FILTER_VALIDATE_FLOAT, $options)); // float(1234567.0)

Сравнение is_numeric() и filter_var()

<?php
declare(strict_types=1);

$values = ['42', '3.14', '1e5', ' 42 ', '42abc', '0x1A', '', 'abc'];

foreach ($values as $val) {
    $isNumeric = is_numeric($val) ? 'true' : 'false';
    $filterInt = filter_var($val, FILTER_VALIDATE_INT) !== false ? 'true' : 'false';
    $filterFloat = filter_var($val, FILTER_VALIDATE_FLOAT) !== false ? 'true' : 'false';

    printf(
        "%-10s | is_numeric: %-5s | FILTER_INT: %-5s | FILTER_FLOAT: %-5s\n",
        "'{$val}'",
        $isNumeric,
        $filterInt,
        $filterFloat,
    );
}

// Output:
// '42'       | is_numeric: true  | FILTER_INT: true  | FILTER_FLOAT: true
// '3.14'     | is_numeric: true  | FILTER_INT: false | FILTER_FLOAT: true
// '1e5'      | is_numeric: true  | FILTER_INT: false | FILTER_FLOAT: true
// ' 42 '     | is_numeric: true  | FILTER_INT: false | FILTER_FLOAT: false
// '42abc'    | is_numeric: false | FILTER_INT: false | FILTER_FLOAT: false
// '0x1A'     | is_numeric: false | FILTER_INT: false | FILTER_FLOAT: false
// ''         | is_numeric: false | FILTER_INT: false | FILTER_FLOAT: false
// 'abc'      | is_numeric: false | FILTER_INT: false | FILTER_FLOAT: false

Ключевое отличие: is_numeric() допускает пробелы и научную нотацию. filter_var() с FILTER_VALIDATE_INT строже -- не допускает пробелы, дробные числа, научную нотацию. Для валидации пользовательского ввода предпочитайте filter_var().

Подводные камни

Строки в условиях

<?php
declare(strict_types=1);

// Strings in boolean context
var_dump((bool) '0');      // false — ONLY non-empty string that is false
var_dump((bool) '');       // false — empty string
var_dump((bool) ' ');      // true  — space is not empty
var_dump((bool) '0.0');    // true  — only '0' is false, not '0.0'
var_dump((bool) 'false');  // true  — string 'false' is truthy
var_dump((bool) 'null');   // true  — string 'null' is truthy
var_dump((bool) '00');     // true  — only exact '0' is false

Восьмеричные и шестнадцатеричные строки

<?php
declare(strict_types=1);

// Hex/octal strings are NOT auto-converted since PHP 7.0
var_dump('0x1A' + 0);      // 0 + Warning — NOT 26
var_dump('0755' + 0);      // int(755) — treated as decimal, NOT octal!
var_dump('0b1010' + 0);    // 0 + Warning — NOT 10

// Use explicit conversion functions
var_dump(intval('0x1A', 16));  // int(26) — hex
var_dump(intval('0755', 8));   // int(493) — octal
var_dump(intval('1010', 2));   // int(10) — binary
var_dump(hexdec('1A'));        // int(26)
var_dump(octdec('755'));       // int(493)
var_dump(bindec('1010'));      // int(10)

Сравнение с нулем

<?php
declare(strict_types=1);

// PHP 8.0+ — safe comparisons with 0
var_dump(0 == null);        // true  — null → 0
var_dump(0 == false);       // true  — false → 0
var_dump(0 == '');          // false — '' is non-numeric
var_dump(0 == '0');         // true  — '0' → 0
var_dump(0 == 'foo');       // false — 'foo' is non-numeric
var_dump(0 == '0.0');       // true  — '0.0' → 0.0

// Always prefer strict comparison
var_dump(0 === null);       // false
var_dump(0 === false);      // false
var_dump(0 === '0');        // false

Строки с пробелами в switch

<?php
declare(strict_types=1);

$value = '0';

// switch uses loose comparison (==)
switch ($value) {
    case false:
        echo 'false';  // This matches! '0' == false → true
        break;
    case '0':
        echo 'zero string';
        break;
}
// Output: 'false' — unexpected!

// Use match instead (PHP 8.0+) — strict comparison
$result = match ($value) {
    false => 'false',
    '0' => 'zero string',  // This matches correctly
    default => 'other',
};
echo $result;  // 'zero string'

Практические паттерны

Безопасная валидация ID

<?php
declare(strict_types=1);

function validateId(mixed $input): int
{
    $id = filter_var($input, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1],
    ]);

    if ($id === false) {
        throw new InvalidArgumentException(
            sprintf('Invalid ID: %s', var_export($input, true))
        );
    }

    return $id;
}

echo validateId('42');    // int(42)
echo validateId(42);      // int(42)
// validateId('0');        // InvalidArgumentException
// validateId('-1');       // InvalidArgumentException
// validateId('abc');      // InvalidArgumentException
// validateId('42.5');     // InvalidArgumentException

Безопасная валидация цены

<?php
declare(strict_types=1);

function validatePrice(mixed $input): float
{
    if (is_string($input)) {
        $input = str_replace([' ', ','], ['', '.'], $input);
    }

    $price = filter_var($input, FILTER_VALIDATE_FLOAT);

    if ($price === false || $price < 0) {
        throw new InvalidArgumentException('Invalid price');
    }

    return round($price, 2);
}

echo validatePrice('19.99');        // float(19.99)
echo validatePrice('1 234,56');     // float(1234.56)
echo validatePrice(42);             // float(42.0)
// validatePrice('free');            // InvalidArgumentException
// validatePrice('-10');             // InvalidArgumentException

Проверь себя

5 из 11

Что вернет `'0755' + 0`?

Что вернет `is_numeric('0x1A')` в PHP 8?

Что вернет `(bool) '0.0'`?

Какое из утверждений о `strict_types` верно?

Что вернет `is_numeric(' 42 ')`?