
Полная версия
PHP 8. Современная разработка: от чистого языка до production

PHP 8. Современная разработка: от чистого языка до production
Глава 1. Не ваш дедушкин PHP
В которой мы хороним стереотипы, настраиваем окружение и пишем первый скрипт, который не стыдно показать.
1.1. Слон в посудной лавке
Спросите любого разработчика, не пишущего на PHP, что он думает об этом языке. С высокой вероятностью вы услышите:
«Это язык для WordPress-поделок.»
«Там в коде HTML вперемешку с SQL, видел в гайде 2005 года.»
«У них стрелка не туда показывает.» (Загадочная, но частая претензия.)
Во многом эта репутация заслужена. PHP родился в 1994 году как набор CGI-скриптов для трекинга посещений резюме. Расмус Лердорф и не планировал создавать язык программирования. А потом проект вырос, оброс глобальными функциями с непоследовательным именованием (strpos vs str_rot13) и стал основой веба: на PHP работают Wikipedia, Facebook (ранний), Slack и огромный кусок интернета.
Но с тех пор прошло три десятилетия. PHP пережил две большие революции:
PHP 7 (2015): полная переработка движка, скорость выросла в разы.
PHP 8 (2020–...): строгая типизация, енумы, match-выражения, readonly-свойства и JIT-компилятор.
Современный PHP — это не скриптование вперемешку с версткой. Это язык со строгой типизацией, атрибутами, асинхронными фреймворками и экосистемой, построенной вокруг Composer. Писать на PHP 8 — значит писать чистый, типобезопасный код, который часто неотличим по выразительности от Kotlin или TypeScript.
Миф: «PHP годится только для блогов».
Реальность: На PHP пишут высоконагруженные API, микросервисы, системы очередей и сложные финансовые приложения. Инструменты статического анализа вроде PHPStan и Psalm давно превзошли аналоги из мира Python и Node.js.
Цель этой книги — показать PHP, который существует сегодня. Не тот, о котором рассказывают старые блог-посты.
1.2. Что нам понадобится
Никаких тяжелых IDE и платных хостингов. Минимальный набор джентльмена:
PHP 8.2 или 8.3. Можно поставить через пакетный менеджер, но мы пойдем модным путем — Docker.
Текстовый редактор. VS Code с плагином Intelephense — золотой стандарт. PhpStorm — если вы готовы к небольшой магии автодополнения.
Composer. Менеджер зависимостей. Придет в следующей главе, но пусть уже стоит.
Быстрый старт с Docker (проверено, работает без танцев):
Создайте папку проекта и в ней файл docker-compose.yml:
yaml
version: '3.8'
services:
app:
image: php:8.3-cli
volumes:
- .:/app
working_dir: /app
Запуск: docker compose run --rm app php script.php.
Всё. PHP работает, ничего лишнего не протекло в систему.
Для тех, кто не дружит с Docker: прямой дистрибутив с php.net и команда php -S localhost:8000 (встроенный веб-сервер) — честный способ начать.
1.3. Первый код. Не «Hello, World», а знакомство с типами
Традиционный hello world скучен. Давайте сразу покажем, чем дышит современный PHP:
php
declare(strict_types=1);
function greet(string $name, ?string $title = null): string
{
$prefix = $title ?? 'дорогой';
return "Привет, {$prefix} {$name}!";
}
echo greet('Алексей', 'уважаемый'); // Привет, уважаемый Алексей!
echo greet('Мария'); // Привет, дорогой Мария!
Что здесь важно заметить сразу:
declare(strict_types=1) — с этой строки мы включаем режим строгой типизации. PHP больше не будет неявно превращать "123abc" в 123 при передаче в функцию, ожидающую int. Это первая привычка, которую мы вырабатываем. Она спасет от багов, которые сложно дебажить.
Сигнатура функции greet(string $name, ?string $title = null): string — здесь указаны типы всех параметров и возвращаемого значения. ?string означает «строка или null». Такие объявления — не комментарии для красоты. PHP проверяет их во время выполнения. Если передать int вместо string — скрипт упадет с ясной ошибкой TypeError, а не продолжит тихо творить дичь.
Оператор ?? (null coalescing) — изящный способ сказать «если левое значение null, бери правое». Никаких isset($title) ? $title : 'дорогой'.
Интерполяция переменных в двойных кавычках работает, но мы используем её осознанно, а не для вывода сырого HTML.
1.4. Как PHP исполняет код (краткий ликбез)
PHP — не компилируемый язык в классическом понимании. Запрос приходит — PHP исполняет скрипт. Но внутри всё хитрее:
Код превращается в OPcache (байт-код) и кешируется в памяти. Нет перечитывания файлов на каждый чих.
Начиная с PHP 8.0, в ядре работает JIT-компилятор, который может транслировать горячие участки кода прямо в машинные инструкции. Это радикально ускоряет вычислительные задачи (машинное обучение на чистом PHP всё еще сомнительно, но обработка изображений или сложная математика — уже да).
Нам для старта это знание нужно, чтобы понимать: PHP — не медленный интерпретатор-черепаха. Он быстр, предсказуем и идеально заточен под модель «запрос-ответ». А с развитием проектов вроде Swoole и RoadRunner он умеет работать и как долгоживущий процесс, не теряя состояния между запросами.
1.5. План на книгу (без спойлеров)
В этой книге мы не будем строить фреймворк-убийцу. Мы напишем реальный проект — менеджер личных финансов. Через этот проект мы пройдем:
Главы 2-4: фундамент типов, ООП и структуры данных.
Главы 5-8: база данных, формы, API и защита от угона кошелька.
Главы 9-12: архитектура без магии (DI, консольные команды, тесты).
Главы 13-15: Composer, продвинутый тулинг и взгляд в асинхронность.
Каждая глава заканчивается практическим заданием. Например, к концу этой главы задание будет звучать так:
Задание: установить PHP, запустить скрипт с объявленной функцией, и намеренно передать в неё неверный тип, чтобы увидеть TypeError. Прочувствовать, как строгая типизация ловит ошибку до того, как она превратится в «а почему баланс пользователя стал NaN?».
Вопрос к тебе:
Глава 2. Типы — это не скучно
В которой мы выясняем, почему mixed — не ругательство, а union types — не профсоюз, и учим PHP проверять наш код до того, как он упадёт на продакшене.
2.1. Зачем нам типы, если PHP динамический?
PHP всегда был языком с динамической типизацией. Переменная $x могла быть строкой, потом числом, потом объектом класса DatabaseConnection, и никто не жаловался. Кроме тех, кто это потом дебажил в три часа ночи.
Проблема динамической типизации — не в скорости, а в молчаливых ошибках:
php
// Классика. Без строгой типизации.
function calculateDiscount(float $price, int $percent): float
{
return $price * ($percent / 100);
}
calculateDiscount('100', 'десять'); // Что произойдёт?
// PHP молча приведёт '100' к 100.0, а 'десять' к 0.
// Результат: 0.0. Ни ошибки, ни предупреждения. Баг в боевом кошельке.
Со strict_types=1 этот код выбросит TypeError ещё на входе в функцию. Не нужно писать ручную валидацию каждого параметра. PHP делает это сам.
Вывод: типы в сигнатурах — это не про бюрократию. Это про то, чтобы проблемы обнаруживались в момент вызова, а не в момент, когда пользователь видит пустой баланс.
2.2. Скалярные типы и их нюансы
Базовые типы: int, float, string, bool. Казалось бы, что тут говорить. Но дьявол в деталях.
int и float
php
declare(strict_types=1);
function divide(int $a, int $b): float
{
return $a / $b;
}
echo divide(5, 2); // 2.5
// Возвращаемый float принимается без вопросов.
// А так — нет:
function getInt(): int
{
return 5.7; // TypeError: Return value must be of type int, float returned.
}
PHP не округлит 5.7 до 6 при возврате, даже если вам кажется, что это логично. Строгая типизация означает строгую.
bool: правда и ложь по-человечески
php
function isActive(bool $status): string
{
return $status ? 'Активен' : 'Неактивен';
}
isActive(1); // TypeError. 1 — не bool.
isActive('true');// TypeError. 'true' — не bool.
Только true и false. Никаких 0, 1, 'yes'. Мозг расслабляется, глаза не дёргаются.
string: осторожно с null
php
function sayHello(string $name): string
{
return "Hello, {$name}";
}
sayHello(null); // TypeError без ?string.
Если параметр может быть null, мы обязаны сказать об этом явно (до этого доберёмся через минуту).
2.3. Union Types: когда одного типа мало
С PHP 8.0 можно указать, что переменная принимает значения нескольких типов. Синтаксис: типA|типB.
php
function formatBalance(int|float $amount): string
{
return number_format($amount, 2, '.', ' ') . ' ';
}
formatBalance(1500); // "1 500.00 "
formatBalance(99.5); // "99.50 "
formatBalance('100'); // TypeError: string is not int|float.
До PHP 8.0 пришлось бы писать два отдельных метода или принимать всё через mixed и проверять руками. Union types — это документация, которую компилятор проверяет за вас.
Частый кейс: параметр или null
php
function findUser(int $id): ?User
{
// Возвращаем User или null, если не нашли.
}
?User — синтаксический сахар для User|null. Оба варианта равнозначны, но ?Type короче и привычнее для глаз. Важно: ?int $x = null — значение по умолчанию null обязательно, иначе PHP заставит передать аргумент явно.
2.4. Mixed: осознанная тишина
mixed — тип, который говорит: «я не знаю или мне всё равно, что здесь окажется». Это не ошибка, это намеренное решение.
php
function logAnything(mixed $data): void
{
file_put_contents('app.log', print_r($data, true), FILE_APPEND);
}
logAnything(['user' => 'admin']);
logAnything('Какой-то текст');
logAnything(42);
logAnything(new Exception('Ой'));
Когда использовать mixed:
Логирование и дебаг-утилиты.
Методы, которые действительно принимают что угодно (например, сериализаторы).
Переходный период при рефакторинге легаси.
Когда НЕ использовать:
В сигнатурах бизнес-логики. Если метод createOrder принимает mixed, что-то пошло не так.
2.5. Типы возврата: void, never и static
void
Функция ничего не возвращает (точнее, возвращает null, но PHP запрещает использовать этот null):
php
function sendEmail(string $to): void
{
// Отправляем письмо и молчим.
// return null; — ошибка.
// return; — ок.
}
never
Новый тип с PHP 8.1. Функция или метод никогда не завершается нормально. Она либо бросает исключение, либо вызывает exit().
php
function redirect(string $url): never
{
header("Location: {$url}");
exit();
}
function throwNotFound(): never
{
throw new \RuntimeException('Не найдено');
}
Зачем это нужно? Статический анализатор видит never и понимает: код после вызова этой функции недостижим. Если вы написали:
php
$user = findUser($id) ?? throwNotFound();
// Здесь $user точно не null. PHPStan это знает.
static
Используется в наследовании. Если метод возвращает static, значит, он возвращает экземпляр того класса, у которого вызван, а не того, где метод определён.
php
class QueryBuilder
{
public function where(string $clause): static
{
// Добавляем условие и возвращаем себя.
return $this;
}
}
class UserQuery extends QueryBuilder
{
public function active(): static
{
return $this->where('active = 1');
}
}
$query = (new UserQuery())->active();
// $query — экземпляр UserQuery, не QueryBuilder.
Это фундамент для текучих интерфейсов (fluent API) и правильного автодополнения в IDE.
2.6. Типы свойств класса
С PHP 7.4 можно объявлять типы у свойств класса. С PHP 8.1 — делать их readonly.
php
class Money
{
public readonly float $amount;
public readonly string $currency;
public function __construct(float $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}
}
$m = new Money(100.0, 'RUB');
$m->amount = 200.0; // Error: Cannot modify readonly property.
Нюансы:
readonly можно установить только один раз (обычно в конструкторе).
Неприменимо к свойствам со значением по умолчанию в объявлении.
Объект целиком становится неизменяемым по этому свойству — кирпичик в сторону иммутабельности.
2.7. Проверка типов: instanceof и не только
Для объектов:
php
if ($user instanceof Admin) {
// Делаем админские штуки.
}
Для массивов и сложных структур instanceof бесполезен. Тут в будущем помогут енумы, интерфейсы и коллекции (глава 4 заложит фундамент).
Пока запомним: если метод возвращает User|null, мы обязаны проверить результат перед использованием:
php
$user = $repository->find($id);
if ($user === null) {
throw new \RuntimeException('Пользователь не найден');
}
// Далее $user — точно User.
Или изящнее (PHP 8.0+):
php
$user = $repository->find($id) ?? throw new \RuntimeException('Не найден');
Оператор ?? + throw — коротко, ясно и без лишних if.
2.8. Практическое задание
Создайте функцию calculateTotal(array $items, float $discount = 0.0): float.
$items — массив цен (float).
$discount — скидка в процентах.
Включите strict_types=1.
Вызовите функцию с корректными данными.
Вызовите её с массивом, содержащим строку вместо float, и зафиксируйте TypeError.
Напишите метод transfer(Money $from, Money $to, int|float $amount): void.
Проверьте, что $amount больше нуля.
Если валюты $from и $to не совпадают — выбросьте исключение.
Объявите класс Temperature с readonly-свойством float $celsius и методом toFahrenheit(): float. Создайте объект и попробуйте изменить свойство после создания. Прочувствуйте момент.
Глава 3. Функции, замыкания и всё, чтомежду
В которой мы прощаемся с switch из девяностых, учим функции летать без имени и понимаем, почему array_map — не просто велосипед для хипстеров.
3.1. Именованные функции: переосмысление
Именованные функции в PHP вы знаете. Но даже в них PHP 8 принёс изменения, которые делают код чище.
Именованные аргументы (PHP 8.0)
Раньше порядок параметров был догмой. Теперь можно передавать аргументы по имени:
php
function createInvoice(
string $client,
float $amount,
string $currency = 'RUB',
bool $paid = false,
): string {
// ...
}
// Можно так:
createInvoice(
client: 'ООО Рога',
amount: 15000.00,
paid: true,
);
// Или даже вразнобой (но лучше так не злоупотреблять):
createInvoice(
paid: false,
amount: 5000.00,
client: 'ИП Копыта',
);
Преимущества:
Код самодокументирован.
Можно пропускать необязательные параметры в середине, не передавая значения по умолчанию для предыдущих.
При рефакторинге — меньше шанс перепутать местами два string-параметра и получить трудноуловимый баг.
Вариативные аргументы: ...$args
Если функция принимает произвольное количество аргументов одного типа:
php
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
sum(1, 2, 3, 4); // 10
sum(); // 0
// А так — TypeError:
sum(1, 'два', 3);
Распаковка работает и в обратную сторону:
php
$data = [1, 2, 3];
sum(...$data); // 6
Callback-тип: callable
Когда функция ожидает другую функцию:
php
function applyDiscount(array $prices, callable $discountFn): array
{
return array_map($discountFn, $prices);
}
applyDiscount([100, 200], fn(float $p) => $p * 0.9);
// [90.0, 180.0]
callable принимает строки с именами функций, массивы вида [$object, 'method'], анонимные функции и стрелочные. Но статический анализатор предпочтёт более конкретный тип: \Closure.
3.2. Анонимные функции и замыкания
Анонимная функция — это функция без имени. Но её настоящая сила в замыкании — способности захватывать переменные из внешней области видимости.
php
$multiplier = 2.5;
$fn = function (float $value) use ($multiplier): float {
return $value * $multiplier;
};
$fn(10); // 25.0
use захватывает переменную по значению на момент определения. Сюрприз для новичков:
php
$x = 1;
$fn = function () use ($x): int {
return $x;
};
$x = 99;
echo $fn(); // 1, не 99!
Если нужно захватить по ссылке — use (&$x). Но с этим осторожно: изменяемое состояние из замыкания — источник трудноотлаживаемых багов. Современный подход — не мутировать переменные, а возвращать новые значения.
Замыкание как фабрика
Классический пример — создание параметризованных функций:
php
function makeMultiplier(float $factor): \Closure
{
return fn(float $value): float => $value * $factor;
}
$double = makeMultiplier(2);
$triple = makeMultiplier(3);
$double(5); // 10
$triple(5); // 15
Стрелочная функция fn() => ... автоматически захватывает переменные по значению. use не нужен. Код лаконичен до предела.
3.3. Стрелочные функции: синтаксис и границы
С PHP 7.4 появился короткий синтаксис:
php
// Было:
$fn = function (int $x) use ($y): int {
return $x + $y;
};
// Стало:
$fn = fn(int $x): int => $x + $y;
Особенности стрелочных функций:
Всегда возвращают значение (одно выражение, без return).
Автозахват переменных по значению.
Не могут содержать несколько выражений или блоков { }. Для сложной логики — обычные анонимные функции.
Где применять: в array_map, array_filter, usort и любых колбэках, где логика умещается в одно выражение.
php
$users = [
['name' => 'Анна', 'age' => 28],
['name' => 'Борис', 'age' => 35],
];
$names = array_map(fn(array $u): string => $u['name'], $users);
// ['Анна', 'Борис']
$adults = array_filter($users, fn(array $u): bool => $u['age'] >= 30);
// Только Борис.
3.4. match-выражение: замена switch
switch страдает от двух проблем: требует break и может случайно проваливаться. match (PHP 8.0) решает обе:
php
function getStatusText(int $code): string
{
return match($code) {
200 => 'OK',
301 => 'Moved Permanently',
404 => 'Not Found',
500 => 'Internal Server Error',
default => 'Unknown',
};
}
Что важно:
Строгое сравнение (===). match('200') не совпадёт с 200.
Ошибка, если ни одна ветка не подошла и нет default.
Каждая ветка — одно выражение (можно блок с => и фигурными скобками для многострочной логики).
Возвращает значение, поэтому прямо в return.
Продвинутый пример с условиями:
php
function classifyNumber(int $n): string
{
return match(true) {
$n < 0 => 'Отрицательное',
$n === 0 => 'Ноль',
$n < 10 => 'Однозначное',
$n < 100 => 'Двузначное',
default => 'Большое',
};
}
Шаблон match(true) позволяет использовать произвольные условия, не ограничиваясь одним значением для сравнения.
3.5. Функции высшего порядка и композиция
Функция высшего порядка — та, что принимает или возвращает другую функцию. Мы уже видели makeMultiplier(). Теперь пойдем дальше:
Композиция функций (ручная):
php
$formatPrice = fn(float $amount): string => number_format($amount, 2) . ' ';
$applyTax = fn(float $amount): float => $amount * 1.2;
$roundUp = fn(float $amount): float => ceil($amount);
// Композиция: округлить добавить налог отформатировать
$process = fn(float $a): string => $formatPrice($applyTax($roundUp($a)));
$process(99.1); // "119.00 "
Пока выглядит как матрёшка. Во второй части книги мы напишем пайплайн-обработчик, который сделает это читаемым. Пока важно понять идею: функции можно собирать как конструктор, маленькие и предсказуемые.









