`declare(strict_types=1)` — це директива рівня одного файлу, яка вимикає приведення скалярів для викликів, зроблених із цього файлу: без неї PHP у coercive-режимі намагається сконвертувати аргумент до оголошеного типу (з PHP 8.0 нечислові рядки — це `TypeError`, а числові з «хвостом» — `int` плюс `E_WARNING`), зі strict приймається лише точний тип, з єдиним винятком `int` → `float`.
declare(strict_types=1) — це не налаштування рантайму й не ini-опція, а директива часу компіляції з областю дії рівно в один файл. Вона зʼявилась у PHP 7.0 разом зі скалярними тайп-хінтами (RFC Andrea Faulds) і перемикає перевірку оголошених типів між двома режимами: coercive (за замовчуванням) і strict. Записувати її треба першою інструкцією файлу — після <?php не може бути ані пробільного виводу, ані namespace, ані use; аргумент — тільки літерал 0 або 1, змінна там дасть помилку компіляції. Директива не успадковується: include строгого файлу з нестрогого нічого не змінює, автозавантажений клас живе за режимом свого файлу, і саме тому найпоширеніша помилка — поставити рядок в один bootstrap.php і вважати проєкт строгим.
Найважливіша механіка: режим бере той файл, у якому знаходиться виклик, а не той, де функція оголошена. PHP на етапі компіляції позначає опкод виклику прапорцем поточного файлу, і перевірка аргументів у момент виконання йде за цим прапорцем. Практичний наслідок — ваш строгий код, викликаючи бібліотеку без директиви або взагалі внутрішню функцію ядра, отримає строгу перевірку: strlen(42) зі strict-файлу кине TypeError, хоча strlen() до вашого файлу не має жодного стосунку. І навпаки: якщо строга бібліотека викликає ваш нестрогий callback, перевірка буде строгою, бо виклик стався в її файлі.
У coercive-режимі PHP намагається сконвертувати аргумент до оголошеного типу, і з PHP 8.0 ці правила стали значно чеснішими. Повністю числовий рядок ('5', ' 5 ', '1e3') конвертується тихо. Leading-numeric рядок ('5 apples') конвертується у 5, але з E_WARNING. Повністю нечисловий рядок ('apples') з PHP 8.0 — це вже TypeError, тоді як у 7.x він мовчки ставав 0 і саме через це в базу потрапляли записи з id = 0. Окремо PHP 8.1 задеприкейтив неявне float → int із втратою дробової частини: 8.5 у параметр int дає deprecation-повідомлення. У strict-режимі всього цього немає взагалі — приймається лише точний тип, з єдиним винятком: int можна передати в параметр float (widening), бо в PHP немає окремого літерала для «цілого числа з плаваючою комою» і вимагати 100.0 замість 100 було б знущанням. Зворотний напрямок, float → int, заборонений завжди.
Дуже поширене перебільшення на співбесіді — сказати, що strict_types «вимикає type juggling». Не вимикає. Директива діє лише там, де тип оголошено явно: параметри, тип повернення, типізовані властивості (PHP 7.4+) і типізовані константи класів (PHP 8.3+). Усе інше поводиться як завжди: '5' + 5 дає 10, if ($string) приводить до bool, == порівнює за своїми правилами (які, до речі, самі змінились у PHP 8.0 у RFC «Saner string to number comparisons»), ключ масиву '5' збігається з 5, а array_sum() підсумує змішаний масив без питань. Не чіпає директива й нетипізовані параметри — там просто нема чого перевіряти. Тому strict_types корисно розуміти вузько: це заборона неявного приведення скалярів у типізованих сигнатурах, не більше.
Практично це означає дві речі. Перша — вмикати директиву треба автоматично в кожному файлі: правило declare_strict_types у Pint/PHP-CS-Fixer, шаблон файлу в IDE, перевірка в CI; ручна дисципліна тут не працює, а частково строгий проєкт дає найгірший варіант — поведінка залежить від того, з якого файлу прийшов виклик. Друга — межа з зовнішнім світом. HTTP-параметри, значення з CLI, дані з БД у деяких драйверах, JSON без JSON_BIGINT-нюансів — усе це приходить рядками, і в строгому коді конвертація має бути явним, видимим кроком: filter_var(..., FILTER_VALIDATE_INT), FormRequest з правилом integer, $request->integer('page'). Строгі типи не роблять валідацію за вас — вони гарантують, що після точки конвертації неправильний тип уже не протече глибше в домен. І варто памʼятати про верхній шар: PHPStan рівня 6+ ловить ті самі невідповідності статично, до запуску, тоді як strict_types спрацьовує вже в рантаймі — ці два інструменти доповнюють один одного, а не замінюють.
<?php
declare(strict_types=1); // ПЕРША інструкція файлу, інакше fatal error
final class Cart
{
public int $itemCount = 0; // типізована властивість теж під strict
public function addItem(int $productId, float $price): float
{
$this->itemCount++;
return $price * $productId;
}
}
$cart = new Cart();
// int → float: ЄДИНЕ послаблення, дозволене і в strict-режимі (widening)
$cart->addItem(7, 100); // 100 стає 100.0, помилки немає
try {
// рядок у параметр int: у strict-режимі приведення заборонене
$cart->addItem('7', 100.0); // TypeError: Argument #1 must be of type int, string given
} catch (TypeError $e) {
echo $e->getMessage();
}
try {
// float → int заборонено ЗАВЖДИ, навіть без strict_types
$cart->addItem(7.0, 100.0); // TypeError: Argument #1 must be of type int, float given
} catch (TypeError $e) {
echo $e->getMessage();
}
try {
$cart->itemCount = '10'; // типізована властивість: теж TypeError
} catch (TypeError $e) {
echo $e->getMessage();
}
// Директива не чіпає type juggling поза оголошеними типами:
var_dump('5' + 5); // int(10) — арифметика працює як раніше
var_dump('abc' == 0); // false у PHP 8+, true у PHP 7 — це вже інші правила
// Межа з HTTP: вхід завжди рядок, конвертація має бути явною
$page = filter_var($_GET['page'] ?? '1', FILTER_VALIDATE_INT) ?: 1;
$cart->addItem($page, 9.99); // сюди приходить справжній int
Формулювання, яке закриває питання: «strict_types — це директива компіляції на один файл, і вирішує вона файл виклику; вмикає не заборону type juggling, а заборону неявного приведення скалярів у типізованих сигнатурах — плюс один виняток, int → float». Далі додайте, що в проєкті це ставлять у кожен PHP-файл автоматично (Pint/PHP-CS-Fixer, правило `declare_strict_types`), а на межі з HTTP усе одно потрібен явний каст.