Перейти к основному содержимому

5.07. Переменные и типы данных

Разработчику Архитектору

Переменные и типы данных

PHP — скриптовый язык общего назначения, изначально созданный для веб-разработки, но впоследствии значительно расширивший свои возможности. Одной из ключевых особенностей PHP является его динамическая типизация, что означает: тип данных переменной определяется интерпретатором — в процессе выполнения программы, на основании присваиваемого значения. Это упрощает написание кода для быстрого прототипирования и веб-сценариев, но требует от разработчика повышенного внимания к семантике операций, особенно при сравнениях, арифметике и передаче данных между слоями приложения.

В настоящей главе последовательно рассматриваются:

  • природа переменных в PHP (лексика, область видимости, особенности именования);
  • система типов: скалярные, составные и специальные типы;
  • механизм неявного и явного преобразования типов;
  • инструменты проверки и работы с типами;
  • последствия динамической типизации для надёжности и читаемости кода;
  • практики, способствующие снижению рисков, связанных с нестрогой типизацией.

1. Переменные: синтаксис и семантика

В PHP переменная — это именованный контейнер для временного хранения значения в памяти. Все переменные в PHP начинаются со знака доллара ($), за которым следует идентификатор. Идентификатор должен соответствовать следующим правилам:

  • первый символ после $ — буква латинского алфавита (a–z, A–Z) или символ подчёркивания (_);
  • последующие символы могут включать буквы, цифры (0–9) и подчёркивания;
  • регистр имеет значение: $name, $Name и $NAME — три разные переменные.

Примеры корректных имён:

$userName
$_cacheKey
$totalAmount2024

Примеры некорректных имён:

$1user      // начинается с цифры — синтаксическая ошибка
$user-name // дефис недопустим — интерпретируется как вычитание
$user name // пробел недопустим — синтаксическая ошибка

В отличие от некоторых других языков (например, C# или Java), в PHP отсутствует обязательное объявление переменной перед использованием. Переменная считается созданной в момент первого присваивания значения. Если переменная используется до присваивания, PHP генерирует предупреждение уровня E_NOTICE, а само выражение ведёт себя так, будто переменная содержит значение null. Это поведение, с одной стороны, упрощает написание небольших сценариев, с другой — повышает риск опечаток и трудноуловимых логических ошибок. Поэтому настоятельно рекомендуется включать режим отладки с полным набором отчётов об ошибках (error_reporting = E_ALL) на всех этапах разработки, включая тестирование.

Область видимости переменных в PHP определяется контекстом: глобальная, локальная (внутри функции), статическая (сохраняющая значение между вызовами функции), или суперглобальная (предопределённые массивы, такие как $_GET, $_POST, $_SERVER). Переменные, объявленные вне функций и классов, существуют в глобальной области видимости. Внутри функций по умолчанию доступны только локальные переменные; для доступа к глобальной переменной необходимо явно использовать ключевое слово global или обращаться через суперглобальный массив $GLOBALS. Такое поведение способствует инкапсуляции, но требует осознанного управления состоянием.

В PHP переменные реализованы как zval (Zend value) — внутренняя структура данных Zend Engine, содержащая само значение, его тип, счётчик ссылок и флаги. Это позволяет эффективно реализовать семантику копирования при записи (copy-on-write): при присваивании одной переменной другой на уровне PHP создаётся новая ссылка на тот же zval; копирование происходит только в момент модификации. Таким образом, PHP балансирует между удобством высокоуровневых операций и производительностью низкоуровневого выполнения.


2. Система типов в PHP

PHP поддерживает восемь типов данных, которые условно делятся на три категории:

2.1. Скалярные типы (scalar types)

К ним относятся значения, представляющие единичные, неделимые сущности:

  • boolean — логический тип, принимающий ровно два значения: true и false. В контекстах, ожидающих булево значение (например, в условных операторах if, while), PHP применяет правила приведения к булеву типу (boolean conversion). Следующие значения считаются ложными (false):
    false, 0 (целое), 0.0 (вещественное), пустая строка "" или строка "0", пустой массив [], null, а также объекты некоторых расширений (например, SimpleXML с пустым документом).
    Все остальные значения интерпретируются как true.

  • integer — целое число со знаком. Диапазон зависит от разрядности системы: на 32-битных платформах — от −2 147 483 648 до 2 147 483 647; на 64-битных — от −9 223 372 036 854 775 808 до 9 223 372 036 854 775 807. PHP поддерживает запись целых чисел в десятичной, шестнадцатеричной (0x...), восьмеричной (0...) и двоичной (0b...) системах счисления. Переполнение целого числа автоматически преобразует его в тип float, что может привести к потере точности.

  • float (также известен как double или real) — число с плавающей запятой. Реализован в соответствии со стандартом IEEE 754 двойной точности (64 бита). Следует помнить, что числа с плавающей запятой не могут точно представлять все десятичные дроби (например, 0.1 + 0.2 !== 0.3), что является фундаментальным ограничением двоичной арифметики. Для финансовых расчётов рекомендуется использовать расширение bcmath или хранить денежные суммы в минимальных единицах (например, копейках) как целые числа.

  • string — последовательность байтов, интерпретируемых как текст. Строка может быть в UTF-8, Windows-1251, KOI8-R и т.д. Важно, что встроенные строковые функции (например, strlen(), substr()) работают с байтами, а не с символами Unicode. Для корректной работы с многобайтовыми кодировками (в первую очередь UTF-8) следует использовать функции из расширения mbstring (mb_strlen(), mb_substr() и др.). Строки могут быть объявлены в одинарных ('...'), двойных ("...") кавычках или с использованием синтаксиса heredoc/nowdoc. В двойных кавычках и heredoc поддерживаются интерполяция переменных и escape-последовательности (например, "\n"), в одинарных — только escape-последовательности \' и \\.

2.2. Составные типы (compound types)

  • array — упорядоченная коллекция пар ключ–значение. Ключом может быть целое число (integer) или строка (string); значением — любой тип, включая другой массив (вложенность не ограничена). Массивы в PHP объединяют функциональность списков, словарей, хэш-таблиц и множеств. Существует два основных вида: индексированные (ключи — последовательные целые числа, начиная с 0) и ассоциативные (ключи — произвольные строки или числа). С версии PHP 7.4 появилась поддержка типизированных массивов в объявлениях свойств классов (array<string, int>), а с PHP 8.0 — union-типов и mixed, что улучшает статическую проверку.

  • object — экземпляр класса. Объекты передаются по ссылке (начиная с PHP 5; в PHP 4 передавались по значению). Объект может содержать свойства и методы, наследоваться от других классов, реализовывать интерфейсы. Даже если все свойства объекта удалены или имеют значение null, сам объект не равен null: new stdClass() !== null.

2.3. Специальные типы

  • null — единственный возможный тип значения null. Обозначает отсутствие значения. Переменная имеет тип null, если:
    — ей явно присвоено null;
    — она объявлена, но ей ещё не присвоено никакое значение;
    — она была уничтожена с помощью unset().
    PHP не различает необъявленную и объявленную, но имеющую значение null переменную при чтении — в обоих случаях возникает E_NOTICE, а результат выражения — null. Однако функция isset() вернёт false и в первом, и во втором случае, тогда как array_key_exists() или оператор ?? (null coalescing) позволяют различать «отсутствие» и «явное null» в массивах и свойствах объектов.

  • resource (устаревший) — специальный тип, представляющий ссылку на внешний ресурс: дескриптор файла, соединение с БД, изображение GD и т.п. Начиная с PHP 8.0 многие ресурсы были заменены на объекты (например, mysqli вместо ресурса mysql), и тип resource постепенно выводится из употребления. Тем не менее, в унаследованном коде он встречается часто. Ресурсы автоматически освобождаются при уничтожении переменной, но рекомендуется закрывать их явно (например, fclose(), mysqli_close()), особенно в долгоживущих скриптах.

Дополнительно, начиная с PHP 8.0, введён псевдотип mixed, обозначающий «любой тип», и с PHP 8.1 — never (для функций, которые никогда не возвращают управление). Эти типы используются в строгих объявлениях типов и аналитических инструментах, но не влияют на runtime-поведение.


3. Преобразование типов: неявное и явное

Одна из самых дискуссионных особенностей PHP — его активное использование неявного (автоматического) приведения типов. Интерпретатор стремится избежать фатальных ошибок при выполнении операций с несовместимыми типами, поэтому вместо прерывания выполнения он пытается преобразовать операнды к «подходящему» типу в соответствии с внутренними правилами. Эти правила последовательно закреплены в документации и реализованы в Zend Engine; однако их сложность и контекстная зависимость часто становятся источником неожиданного поведения.

3.1. Контексты, вызывающие неявное приведение

Неявное преобразование происходит в следующих ситуациях:

  • Арифметические операции (+, -, *, /, %, **).
    Все операнды приводятся к числу — либо integer, либо float, в зависимости от содержимого. Если строка начинается с цифр (возможно, с ведущих пробелов), она интерпретируется как число до первого недопустимого символа: "123abc"123, "12.3e2xyz"1230.0. Строка, не начинающаяся с цифры (например, "abc123"), превращается в 0. Пустая строка "" и строка "0" также дают 0, но с разными флагами внутреннего представления — это может повлиять на последующее сравнение.

  • Логические операции и условия (&&, ||, !, if, while, ?:).
    Все значения приводятся к boolean по правилам, описанным ранее. Особенно важно помнить, что строка "0" — единственная непустая строка, которая считается ложной.

  • Строковый контекст (конкатенация ., echo, print, интерполяция в двойных кавычках).
    Значения приводятся к строке. Для null результат — пустая строка ""; для boolean"1" (true) или "" (false); для integer/float — десятичное представление; для array или object без метода __toString() — ошибка уровня E_RECOVERABLE_ERROR (начиная с PHP 8.0 — TypeError).

  • Сравнение с оператором равенства (==).
    PHP приводит оба операнда к общему типу, руководствуясь таблицей loose comparison. Например, "0" == falsetrue, 0 == "abc"true, "123" == 123true, но "123a" == 123true, а "a123" == 123false. Такое поведение делает == крайне ненадёжным для валидации входных данных.

3.2. Явное приведение (кастинг)

Разработчик может управлять преобразованием типов вручную, используя один из трёх механизмов:

  1. Оператор кастинга в скобках ((type)).
    Это наиболее распространённый и производительный способ. Поддерживаемые формы:

    • (int), (integer)
    • (bool), (boolean)
    • (float), (double), (real)
    • (string)
    • (array)
    • (object)
    • (unset) — эквивалентен присваиванию null (устаревшее, не рекомендуется)

    Примеры:

    $str = "  42 apples";
    $num = (int)$str; // 42 — отбрасывается всё после цифр
    $bool = (bool)"0"; // false — строка "0" → false
    $arr = (array)$num; // [0 => 42]
    $obj = (object)['x' => 1]; // объект stdClass со свойством x = 1

    Кастинг — это выражение. Он создаёт новое значение заданного типа.

  2. Функция settype($var, $type).
    Изменяет непосредственно переменную, переданную по ссылке. Возвращает true при успехе, false — при недопустимом типе. Поддерживает те же строковые имена типов, что и оператор кастинга: 'integer', 'double', 'string', 'array', 'object', 'boolean', 'null'.

    $value = "123";
    settype($value, 'integer'); // $value теперь 123 (int)

    Преимущество settype() — возможность динамического указания целевого типа (например, из конфигурации). Недостаток — побочный эффект изменения переменной и чуть более высокие накладные расходы.

  3. Специализированные функции преобразования.
    Некоторые преобразования требуют дополнительной логики и реализованы в виде отдельных функций:

    • intval(), floatval(), strval() — аналоги (int), (float), (string), но принимают второй параметр (основание системы счисления для intval()).
    • boolval() (PHP 5.5+) — явное приведение к boolean.
    • json_encode()/json_decode() — сериализация/десериализация с контролем типов.
    • filter_var() — валидация и санитизация с приведением (например, FILTER_VALIDATE_INT).

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

При операциях с разнотипными операндами PHP выбирает целевой тип по следующим приоритетам (от высшего к низшему):

КонтекстЦелевой типПояснение
Арифметикаfloatintegerfloat имеет приоритет над integer. Если хотя бы один операнд float → результат float.
Сравнение (==)Зависит от пары типовИспользуется внутренняя таблица loose comparison (см. ниже).
Логический контекстbooleanВсё приводится к true/false.
Строковый контекстstringВызывается внутренний механизм преобразования в строку (например, zend_string_from_long).

Для сравнения через == действует следующее упрощённое правило (полная таблица — в официальной документации PHP):

  • Если один из операндов — boolean, оба приводятся к boolean.
  • Иначе, если один из операндов — object, применяется специальная логика (часто — попытка cast к string или int).
  • Иначе, если оба операнда — строки, сравниваются как строки (лексикографически, побайтово).
  • Иначе — оба приводятся к float и сравниваются численно.

Отсюда, например, следует:

  • 0 == "0e12345"true (оба становятся float: 0.0 == 0.0);
  • "1" == "01"false (сравнение как строк: "1""01");
  • true == "php"true ("php"true, true == true).

4. Проверка типов: инструменты и практики

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

4.1. gettype($var)

Функция возвращает строку с именем типа переменной:
"boolean", "integer", "double" (обратите внимание: не "float"), "string", "array", "object", "resource", "NULL".

Это — диагностический инструмент, полезный при отладке. Однако не следует использовать gettype() в условиях ветвления, поскольку:

  • строковое сравнение медленнее прямой проверки;
  • имена типов (например, "double") могут ввести в заблуждение;
  • логика, зависящая от конкретного типа, часто нарушает принципы полиморфизма.

Пример (не рекомендуется в production-коде):

if (gettype($input) === 'string') {
// обработка строки
}

4.2. Семейство функций is_*()

Это — основной механизм проверки типов в runtime. Все функции возвращают true или false. Наиболее важные:

ФункцияПроверяетОсобенности
is_bool($v)boolean
is_int($v), is_integer($v), is_long($v)integerТри синонима.
is_float($v), is_double($v), is_real($v)floatТри синонима.
is_string($v)string
is_array($v)array
is_object($v)object
is_null($v)nullЭквивалент ($v === null)
is_resource($v)resourceУстаревает, но актуален для legacy-кода
is_numeric($v)число или строка, содержащая числоВозвращает true для "123", "12.3", "1e5", но false для "123abc"
is_scalar($v)скалярный тип (bool, int, float, string)
is_callable($v)может ли значение быть вызвано как функцияПроверяет Closure, имя функции, [$obj, 'method'] и др.

Обратите внимание на различие между is_numeric() и (float)-кастингом:
is_numeric("123abc")false, а (float)"123abc"123.0.

4.3. Оператор instanceof

Для объектов ключевой инструмент — оператор instanceof. Он проверяет, является ли объект экземпляром заданного класса, его наследником или реализует указанный интерфейс.

if ($logger instanceof Psr\Log\LoggerInterface) {
$logger->info('Event occurred');
}

Это предпочтительный способ проверки типов объектов — он устойчив к рефакторингу и поддерживает полиморфизм.


5. Сравнение: == против ===

PHP поддерживает два оператора равенства:

  • == (loose equality) — сравнивает значения после неявного приведения типов.
  • === (strict equality) — сравнивает и значение, и тип; возвращает true только если оба операнда идентичны по значению и типу.

Примеры:

"0" == 0;        // true  — loose: string → int
"0" === 0; // false — strict: string ≠ integer

null == false; // true — loose: null → false
null === false; // false — strict: null ≠ boolean

"123" == 123; // true
"123" === 123; // false

true == "1"; // true
true === "1"; // false

Рекомендация: всегда используйте === и !==, за исключением случаев, когда loose-сравнение намеренно и осознанно применяется (например, при обработке пользовательского ввода, где "0" и 0 семантически равнозначны). Особенно критично это при:

  • валидации форм;
  • работе с возвращаемыми значениями функций (некоторые функции возвращают false при ошибке и 0 как корректный результат);
  • сравнении с null (используйте === null, а не == null).

Начиная с PHP 8.0, введён оператор nullsafe (?->), а с PHP 7.0 — оператор null coalescing (??), которые позволяют избежать явных проверок === null в цепочках вызовов и извлечении значений:

$username = $_GET['user'] ?? 'guest';
$length = $user?->getName()?->length ?? 0;

6. Рекомендации по работе с типами в PHP

Несмотря на динамическую природу языка, современный PHP (начиная с версии 7.0) предоставляет всё больше инструментов для повышения типовой дисциплины:

  1. Включайте строгий режим типов через директиву declare(strict_types=1); в начале файла.
    Это заставляет интерпретатор требовать точного совпадения типов в объявлениях параметров и возвращаемых значениях функций (но не влияет на внутренние операции, например, + или ==).
    Пример:

    declare(strict_types=1);
    function add(int $a, int $b): int {
    return $a + $b;
    }
    add(2, "3"); // Fatal error: Argument 2 passed to add() must be of type int, string given
  2. Используйте типизированные объявления в сигнатурах:

    • параметры (function f(string $name));
    • возвращаемые значения (function id(): int);
    • свойства классов (PHP 7.4+ — private string $name;);
    • типизированные свойства в анонимных классах и трейтах.
  3. Предпочитайте скалярные типы (string, int, bool, float) union-типам и mixed, когда это возможно. Union-типы (string|int) допустимы, но их избыток усложняет анализ.

  4. Используйте статические анализаторы — PHPStan, Psalm. Они способны обнаружить несоответствия типов на этапе разработки, даже без strict_types=1. Интеграция в CI/CD позволяет блокировать слияние кода с типовыми ошибками.

  5. Избегайте «магических» приведений в критических местах:

    • не используйте == для сравнения с 0, "", false, null;
    • не полагайтесь на неявное приведение при обработке пользовательского ввода — сначала валидируйте (filter_var, is_numeric), затем кастите;
    • при работе с JSON — указывайте JSON_THROW_ON_ERROR и проверяйте типы после json_decode().
  6. Документируйте ожидаемые типы с помощью PHPDoc, даже если используется строгая типизация:

    /**
    * @param array<string, mixed> $config
    * @return array<string, string>
    */
    function normalizeConfig(array $config): array { /* ... */ }