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

Полная версия

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

Язык: Русский
Год издания: 2026
Добавлена:
Настройки чтения
Размер шрифта
Высота строк
Поля
На страницу:
1 из 2

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 "


Пока выглядит как матрёшка. Во второй части книги мы напишем пайплайн-обработчик, который сделает это читаемым. Пока важно понять идею: функции можно собирать как конструктор, маленькие и предсказуемые.

На страницу:
1 из 2