Все уроки
ЯзыкиНачальный11 мин чтенияВ моём стеке

Современный PHP

PHP 8.x — типизированный, быстрый и выразительный: enum'ы, readonly, match, атрибуты и Composer.

  • PHP 8.x
  • Composer
  • PSR
  • Enums & readonly
Шаг 1/5
Приходит запрос: POST /orders. Router смотрит на URL и метод и выбирает action контроллера, который его обработает.

За 30 секунд

PHP — серверный язык, созданный для веба: приходит запрос, PHP-скрипт собирает ответ, а после этого всё очищается. Современный PHP (8.x) сильно отличается от своей репутации: в нём есть строгие типы, enum'ы, неизменяемые объекты и JIT-компилятор. С Composer для пакетов и стандартами PSR для общих соглашений он лежит в основе фреймворков вроде Laravel и Symfony и значительной части веба.

Почему я это использую

  • Большинство своих бэкендов я написал на PHP с Laravel — от первой продакшен-системы в 2020 году до сегодняшних платформ заказов, e-commerce и CRM, — потому что он позволяет небольшой команде быстро выпустить большой и надёжный API.
  • Мне нравится модель share-nothing в PHP: каждый запрос начинается с чистого листа, поэтому один плохой запрос не может протащить утечку памяти или состояние в следующий.
  • Современные возможности — enum'ы, readonly-классы и типизированные свойства — позволяют писать доменный код таким же явным, как на TypeScript.
  • Хостинг простой и дешёвый: nginx плюс PHP-FPM работают на любом Linux-сервере, что подходит под проекты, которые реально есть у моих клиентов.

Как это работает

01Как PHP выполняет код: PHP-FPM и жизненный цикл запроса

Большая часть PHP в продакшене работает под PHP-FPM (FastCGI Process Manager). Представьте его как команду работников, ожидающих задания. nginx принимает HTTP-запрос, сам отдаёт статические файлы, а PHP-запросы пересылает в FPM по FastCGI. Мастер-процесс управляет пулом воркеров; свободный воркер берёт запрос, выполняет входной скрипт (для Laravel — public/index.php), отправляет ответ обратно и затем выбрасывает все объекты и переменные этого запроса. Это и есть модель share-nothing: между запросами ничего не сохраняется, кроме того, что вы храните вне процесса (база данных, Redis, файлы). OPcache держит скомпилированный байткод ваших файлов в общей памяти, поэтому PHP не разбирает исходники заново на каждый запрос. Размер пула (pm.max_children) ограничивает число одновременно выполняемых запросов, поэтому подбирайте его под объём RAM. Долгоживущие рантаймы — worker mode во FrankenPHP, RoadRunner или Swoole (Laravel Octane) — держат приложение загруженным в памяти ради дополнительной скорости, но тогда нужно не допускать утечки состояния между запросами.

ini
; /etc/php/8.4/fpm/pool.d/app.conf  (example)
[app]
user = app_user
group = app_user
listen = /run/php/php8.4-fpm-app.sock
pm = dynamic
pm.max_children = 20      ; roughly free RAM / average worker size
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500     ; recycle workers to contain slow leaks
 
; php.ini (production)
; opcache.enable=1
; opcache.validate_timestamps=0  ; reload FPM after each deploy

02Типы и возможности PHP 8, которыми стоит пользоваться

Начинайте каждый файл с declare(strict_types=1);, чтобы PHP отказывался молча превращать "5" в 5, когда функция ожидает int. Типизируйте всё: параметры, возвращаемые значения и свойства, включая union-типы (int|string), nullable-типы (?string) и never для функций, которые всегда бросают исключение. PHP 8.0 принёс match (строгий switch-выражение, который бросает исключение, если ничего не совпало), именованные аргументы, nullsafe-оператор ?-> и constructor property promotion. PHP 8.1 добавил enum'ы, readonly-свойства и first-class callables вроде strlen(...). PHP 8.2 добавил readonly-классы, 8.3 — типизированные константы классов и атрибут #[\Override], 8.4 — property hooks и асимметричную видимость (public private(set)), а 8.5 (ноябрь 2025) — pipe-оператор |>, clone-with и #[\NoDiscard]. Выход PHP 8.6 запланирован на конец ноября 2026 года, с partial function application.

php
<?php
declare(strict_types=1);
 
final readonly class Money
{
    public function __construct(
        public int $amount,     // minor units, e.g. cents
        public string $currency,
    ) {}
 
    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }
        return new self($this->amount + $other->amount, $this->currency);
    }
}

03Enum'ы, match и атрибуты

Enum'ы заменяют «магические строки» и константы классов закрытым набором значений. Backed enum (enum OrderStatus: string) сопоставляет каждому case значение, которое можно хранить в базе данных, а для обратного преобразования есть OrderStatus::from('paid'), который бросает исключение на невалидном вводе, или tryFrom, который возвращает null. У enum'ов могут быть методы, и они могут реализовывать интерфейсы, поэтому правила, касающиеся статуса, живут рядом с самим статусом. match естественно сочетается с enum'ами: он сравнивает строго, возвращает значение и бросает UnhandledMatchError, если какой-то case не обработан, поэтому необработанный новый статус падает громко, а не молча. Атрибуты (#[...]) — это структурированные метаданные, прикреплённые к классам, методам или свойствам и читаемые через рефлексию. Фреймворки используют их для маршрутов, валидации, слушателей событий и внедрения зависимостей, заменяя хрупкие аннотации в docblock'ах.

php
<?php
enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Cancelled = 'cancelled';
 
    public function label(): string
    {
        return match ($this) {
            self::Pending => 'Waiting for payment',
            self::Paid => 'Paid',
            self::Cancelled => 'Cancelled',
        };
    }
}
 
$status = OrderStatus::from($row['status']); // throws on invalid value
echo $status->label();

04Composer и автозагрузка

Composer — менеджер пакетов и автозагрузчик PHP. В composer.json перечислены зависимости с ограничениями версий (^12.0 означает «любая 12.x»), а composer.lock фиксирует точные установленные версии. Коммитьте оба файла, чтобы у каждого разработчика и на каждом сервере устанавливался одинаковый код. Автозагрузка означает, что вам никогда не нужно писать require для своих классов: по PSR-4 пространство имён App\Billing\Invoice соответствует файлу src/Billing/Invoice.php, и Composer загружает его при первом использовании. В продакшене устанавливайте с --no-dev, чтобы пропустить инструменты тестирования, и с --optimize-autoloader (или --classmap-authoritative), чтобы построить быструю карту классов. Регулярно запускайте composer audit — или в CI, — чтобы сверять установленные пакеты с известными уязвимостями, и обновляйте через composer update package/name, а не всё сразу.

json
{
  "require": {
    "php": "^8.3",
    "laravel/framework": "^13.0"
  },
  "require-dev": {
    "phpunit/phpunit": "^12.0"
  },
  "autoload": {
    "psr-4": { "App\\": "src/" }
  },
  "config": { "sort-packages": true }
}
 
// Production install:
// composer install --no-dev --optimize-autoloader --no-interaction

05Стандарты PSR и инструменты качества кода

PSR — это PHP Standards Recommendations от PHP-FIG, группы авторов фреймворков. Они позволяют библиотекам разных авторов стыковаться друг с другом. Чаще всего встречаются: PSR-4 (автозагрузка), PSR-3 (интерфейс логгера), PSR-7 и PSR-17 (объекты HTTP-запроса и ответа), PSR-15 (middleware), PSR-11 (DI-контейнеры), PSR-14 (события) и PSR-18 (HTTP-клиент). Для форматирования PSR-12 заменён на PER Coding Style, который инструменты применяют автоматически. Современный PHP-проект обычно добавляет статический анализ через PHPStan или Psalm на высоком уровне в CI, потому что они ловят ошибки типов, о которых сам PHP сообщает только во время выполнения. Добавьте автоматический форматтер (PHP-CS-Fixer или Laravel Pint) и Rector для автоматических обновлений между версиями PHP и фреймворка. С ними PHP в повседневной работе по надёжности близок к компилируемому языку.

bash
# Install quality tools as dev dependencies
composer require --dev phpstan/phpstan laravel/pint rector/rector
 
# Check types (raise the level over time)
vendor/bin/phpstan analyse src --level=8
 
# Format code to the project style
vendor/bin/pint
 
# Preview automated upgrades (e.g. to new PHP syntax)
vendor/bin/rector process src --dry-run
 
# Check dependencies for known vulnerabilities
composer audit

06Советы для продакшена

Держите PHP на поддерживаемой версии: каждый релиз получает два года активной поддержки и ещё два года исправлений безопасности, поэтому смотрите php.net/supported-versions и планируйте обновления, а не ждите аварии. В продакшене выключайте display_errors и логируйте ошибки: показ стектрейсов пользователям раскрывает пути и секреты. Включите OPcache, а поскольку validate_timestamps=0 означает, что PHP не заметит новых файлов, перезагружайте PHP-FPM в рамках каждого деплоя. Запускайте отдельный FPM-пул для каждого сайта под собственным системным пользователем, чтобы скомпрометированный сайт не мог читать файлы другого. Никогда не собирайте SQL конкатенацией строк; используйте prepared statements или query builder вашей ORM. Хэшируйте пароли через password_hash() (bcrypt или Argon2id), никогда не md5 или sha1. Выносите медленную работу — письма, PDF, вызовы сторонних API — в воркеры очередей, чтобы веб-запросы оставались быстрыми.

Когда применять

  • Веб-приложения и REST API, особенно на Laravel или Symfony, где важны продуктивность и огромная экосистема.
  • Контентные сайты, e-commerce, CRM и бизнес-системы с большим количеством админки.
  • Проекты, которым нужен дешёвый и простой хостинг на стандартных Linux-серверах или shared- и managed-хостинге.
  • Команды, которым нужен зрелый фреймворк «всё включено» с очередями, планировщиком, аутентификацией и ORM из коробки.

Когда не стоит

  • Долгоживущие соединения в очень большом масштабе (тысячи WebSocket'ов на сервер); лучше подойдут Node.js, Go или Elixir либо хостинговый сервис вроде Pusher рядом с PHP.
  • Работа, нагружающая CPU, — обработка видео или машинное обучение, — где у Python, Go или Rust лучше библиотеки и производительность.
  • Команда, которая уже хорошо знает другой бэкенд-стек; лучший язык — часто тот, который команда может поддерживать.

Альтернативы

  • Node.js (NestJS, Express, Fastify)

    Один язык на фронтенде и бэкенде, силён в реальном времени и сервисах с интенсивным I/O.

  • Python (Django, FastAPI)

    Отлично подходит для бэкендов с данными, AI и большим количеством скриптов; Django такой же «всё включено», как Laravel.

  • Go

    Компилируемый, очень быстрый и простой в деплое как один бинарник; для типичных CRUD-приложений кода приходится писать больше.

  • Ruby on Rails

    Похожая на Laravel продуктивность за счёт convention over configuration, но во многих регионах меньше кандидатов на рынке.

Лучшие практики

  1. Начинайте файлы с declare(strict_types=1) и типизируйте каждый параметр, возвращаемое значение и свойство.
  2. Используйте backed enum'ы для статусов и типов, а match — для их сопоставления, вместо магических строк.
  3. Делайте value objects readonly (Money, Email, Address), чтобы их нельзя было случайно изменить.
  4. Коммитьте composer.lock, в продакшене устанавливайте с --no-dev --optimize-autoloader и запускайте composer audit в CI.
  5. Запускайте PHPStan или Psalm на высоком уровне и автоматический форматтер на каждый pull request.
  6. Оставайтесь на поддерживаемой версии PHP и обновляйтесь по одной минорной версии за раз с помощью Rector.
  7. Подбирайте размер пулов PHP-FPM под доступную память и запускайте каждый сайт под своим системным пользователем.

Частые ошибки

  • Оставлять display_errors включённым в продакшене и раскрывать стектрейсы, пути к файлам или учётные данные.
  • Склеивать пользовательский ввод в SQL вместо использования prepared statements.
  • Забывать перезагрузить PHP-FPM после деплоя при выключенной проверке timestamp'ов в OPcache, а потом удивляться, почему работает старый код.
  • Ставить pm.max_children намного выше, чем позволяет RAM, из-за чего сервер уходит в swap и тормозит под нагрузкой.
  • Хранить деньги во float вместо целых чисел в минимальных единицах или типа decimal.

Глубже

07OPcache, preloading и JIT изнутри

PHP компилирует каждый исходный файл в опкоды — инструкции, которые исполняет его виртуальная машина. OPcache хранит эти опкоды, а также interned-строки, в разделяемой памяти, которой пользуются все воркеры FPM, так что файл компилируется один раз, а не на каждый запрос. Если кэш переполняется, PHP начинает перекомпилировать файлы, и производительность тихо падает, поэтому подберите opcache.memory_consumption (по умолчанию 128 MB) и opcache.max_accelerated_files (по умолчанию 10000) под размер кодовой базы и следите за hit rate и потерянной памятью через opcache_get_status(). Preloading (opcache.preload, PHP 7.4+, не на Windows) при старте сервера запускает скрипт, который навсегда загружает выбранные классы в память, экономя стоимость линковки на каждом запросе; любое изменение этих файлов требует полного перезапуска FPM. JIT компилирует горячие опкоды в машинный код. Начиная с PHP 8.4 он настраивается через opcache.jit (по умолчанию disable) и opcache.jit_buffer_size (по умолчанию 64M), и его память добавляется к памяти OPcache. Для типичных веб-приложений, упирающихся в I/O, JIT даёт мало, потому что время уходит на ожидание базы данных; он раскрывается на CPU-тяжёлой работе вроде математики над изображениями или парсеров.

ini
; php.ini - production OPcache tuning (example values)
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0      ; reload FPM on deploy
; opcache.preload=/var/www/app/preload.php
; opcache.preload_user=app_user
 
; Only for CPU-heavy workloads; measure before and after
; opcache.jit=tracing
; opcache.jit_buffer_size=64M
 
realpath_cache_size=4096K
realpath_cache_ttl=600

08Долгоживущие воркеры: Octane, FrankenPHP и утечки состояния

Рантаймы в worker-режиме загружают фреймворк один раз, а затем обслуживают тысячи запросов из одного и того же процесса. Это убирает стоимость бутстрапа и может кратно поднять пропускную способность, но ломает гарантию share-nothing, на которую вы раньше полагались. Всё, что хранится в статическом свойстве, синглтоне в контейнере или глобальном массиве, теперь доживает до следующего запроса — возможно, уже другого пользователя. Типичные баги: сервис, кэширующий текущего тенанта или пользователя в свойстве; статический массив, который растёт бесконечно и течёт по памяти; значение конфигурации, изменённое для одного запроса и так и не сброшенное. Правила для worker-режима: каждый раз берите данные конкретного запроса из самого запроса, а не храните их; регистрируйте сервисы уровня запроса как scoped-биндинги, которые сбрасываются между запросами; избегайте статического изменяемого состояния; и пусть рантайм перезапускает воркеры после фиксированного числа запросов (--max-requests в Octane) в качестве страховки. Проводите нагрузочное тестирование с двумя пользователями параллельно и проверяйте, что ни один не видит данных другого.

php
<?php
// Dangerous in worker mode: state survives between requests
final class CurrentTenant
{
    private static ?int $id = null;          // leaks to the next request
    public static function set(int $id): void { self::$id = $id; }
}
 
// Safer: derive it from the request every time
final class TenantResolver
{
    public function __construct(private readonly Request $request) {}
 
    public function id(): int
    {
        return (int) $this->request->attributes->get('tenant_id');
    }
}
// Laravel: $this->app->scoped(TenantResolver::class); // reset per request

09Безопасность изнутри: сессии, хэширование и unserialize

Многие проблемы безопасности в PHP происходят от настроек по умолчанию и устаревших функций, а не от вашей собственной логики. Сессии: выставьте session.cookie_secure, session.cookie_httponly и session.cookie_samesite=Lax, включите session.use_strict_mode, чтобы PHP отвергал идентификаторы сессий, которые он не создавал, и после логина перегенерируйте id через session_regenerate_id(true), чтобы предотвратить фиксацию сессии. Пароли: password_hash с PASSWORD_DEFAULT (сейчас это bcrypt) или PASSWORD_ARGON2ID, проверка через password_verify, которая сравнивает за постоянное время; после успешного логина вызывайте password_needs_rehash, чтобы незаметно обновлять старые хэши. Никогда не передавайте недоверенные данные в unserialize(): специально сформированный ввод может создать объекты произвольных классов и запустить их магические методы — эта техника называется object injection и может привести к удалённому выполнению кода. Используйте вместо этого json_decode или хотя бы unserialize с allowed_classes, равным false. Сравнивайте секреты и HMAC-подписи через hash_equals, а не ===, чтобы избежать утечек по времени. И наконец, держите disable_functions и open_basedir как эшелонированную защиту, а не как основную.

php
<?php
// Login: verify, then upgrade the hash if the algorithm or cost changed
if (password_verify($password, $user->password_hash)) {
    if (password_needs_rehash($user->password_hash, PASSWORD_ARGON2ID)) {
        $user->password_hash = password_hash($password, PASSWORD_ARGON2ID);
        $user->save();
    }
    session_regenerate_id(true); // prevent session fixation
}
 
// Webhook signature: constant-time comparison
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signatureHeader)) {
    http_response_code(401);
    exit;
}
 
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR); // not unserialize()

10Конкурентность и профилирование: Fiber'ы, статус FPM и slow log

Один PHP-запрос выполняется в одном потоке, но есть инструменты, чтобы делать больше одновременно. Fiber'ы (PHP 8.1) — лёгкие, кооперативно планируемые блоки кода, которые можно приостанавливать и возобновлять; напрямую их используют редко, но библиотеки вроде Revolt, AMPHP и ReactPHP строят на них event loop'ы, чтобы один процесс мог параллельно ждать множество сетевых вызовов. Для классических веб-приложений конкурентность обычно означает параллельные HTTP-вызовы (промисы Guzzle или Http::pool в Laravel) и воркеры очередей для фоновых задач. Когда сайт тормозит, сначала посмотрите, а потом предполагайте. Страница статуса FPM (pm.status_path) показывает активные, простаивающие и ожидающие процессы; растущая listen queue означает, что пул слишком мал или запросы слишком медленные. request_slowlog_timeout записывает стек-трейс любого запроса, который работает дольше лимита, и это прямо указывает на блокирующий вызов. Для более глубокого профилирования используйте сэмплирующий профайлер, например SPX, Blackfire или Tideways, или профайлер Xdebug — только в разработке, потому что он сильно замедляет PHP.

ini
; FPM pool: observability
pm.status_path = /fpm-status          ; restrict to localhost in nginx
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/app-slow.log
request_terminate_timeout = 60s       ; kill runaway requests
 
# Read the status page from the server itself
curl -s "http://127.0.0.1/fpm-status?full" | head -20
# Look at: listen queue, max listen queue, max children reached

Реальные ситуации

01

После деплоя всё ещё отдаётся старый код

Проблема

Исправление задеплоено, файлы на диске правильные, но пользователи всё ещё видят старое поведение. Очистка кэша Laravel не помогла, и проблема исчезает, только когда кто-нибудь перезапускает сервер.

Как решить

Это OPcache с opcache.validate_timestamps=0: PHP никогда не проверяет, изменились ли файлы, и продолжает отдавать старые скомпилированные опкоды. Сделайте reload FPM частью деплой-скрипта — после того как новый релиз встал на место: systemctl reload php8.4-fpm, который плавно заменяет воркеры, не теряя запросов. При релизах на основе симлинков также убедитесь, что nginx передаёт $realpath_root в SCRIPT_FILENAME и DOCUMENT_ROOT, иначе realpath-кэш может продолжать резолвить директорию старого релиза. Добавьте эндпоинт версии или заголовок ответа с git-коммитом и проверяйте его в деплой-пайплайне, чтобы устаревший релиз обнаруживался автоматически, а не пользователями.

02

Сервер уходит в swap, а запросы отваливаются по таймауту в часы пик

Проблема

В обеденное время сайт начинает тормозить, сервер уходит в swap, а nginx пишет в логи ошибки 502 и 504. Пул FPM был настроен с pm.max_children = 100 «для надёжности» на машине с 4 GB.

Как решить

Измерьте реальный размер воркера: ps -o rss -C php-fpm8.4 показывает память на воркер; допустим, в среднем это 60 MB. Если после базы данных, Redis и ОС остаётся около 2.5 GB, безопасный максимум — примерно 40 воркеров, а не 100. Соответственно уменьшите pm.max_children, задайте pm.max_requests, чтобы перезапускать текущие воркеры, и включите pm.status_path, чтобы следить за listen queue. Затем включите slow log, чтобы найти запросы, которые дольше всех держат воркеры, — часто это вызовы внешних API или экспорт отчётов, — и перенесите их в очереди. Если очередь и после этого растёт, ответ — больше CPU или ещё один app-сервер, а не больше воркеров.

03

Обновление большого приложения с PHP 8.1 до 8.4

Проблема

У PHP 8.1 закончилась поддержка безопасности, а в кодовой базе сотни тысяч строк, несколько старых пакетов и мало тестов в старых модулях. Бизнес не может позволить себе сломанный checkout.

Как решить

Обновляйтесь по одной минорной версии за раз. Сначала запустите composer why-not php 8.4, чтобы найти пакеты, блокирующие обновление, и обновите их. Добавьте целевую версию в CI как вторую запись матрицы, чтобы тесты шли на обеих версиях. Запустите Rector с наборами правил PHP 8.2, 8.3 и 8.4 в режиме dry-run, просмотрите изменения и применяйте их небольшими pull request'ами. Прогоните PHPStan на текущем уровне на новой версии и на staging считайте deprecation-уведомления в логах ошибками. Сначала задеплойте на один сервер или canary, следите за уровнем ошибок, затем раскатите полностью и уберите старую версию из CI.

Шпаргалка

Показать версию PHP и загруженные ini-файлы
php -v && php --ini
Проверить статус OPcache в конфиге CLI
php -i | grep -E 'opcache.(enable|jit|memory)'
Проверить конфиг FPM, затем плавно перезагрузить
sudo php-fpm8.4 -t && sudo systemctl reload php8.4-fpm
Измерить память воркеров FPM
ps -o rss=,cmd= -C php-fpm8.4 | sort -n | tail
Почему я не могу обновиться?
composer why-not php 8.4
Найти устаревшие прямые зависимости
composer outdated --direct
Установка для продакшена
composer install --no-dev --classmap-authoritative --no-interaction
Проверка безопасности зависимостей
composer audit
Безопасное декодирование внешних данных
json_decode($body, true, 512, JSON_THROW_ON_ERROR)
Сравнение секретов за постоянное время
hash_equals($expected, $given)

В моих реальных проектах

  • Я выпускал API на Laravel на всём — от PHP 7.3 и Laravel 8 (мой первый продакшен-портал в 2020 году) до PHP 8.3 и Laravel 13 в CRM для сети спортзалов.
  • Платформа зоотоваров использует около 50 PHP enum'ов и примерно 110 сервисных классов, цены смоделированы как value objects, а бэкенд покрыт 95 файлами тестов.
  • При переносе магазина с WordPress на Laravel я сохранил совместимость со старыми хэшами паролей, чтобы существующие клиенты могли войти без принудительного сброса.
  • На серверах, которыми я управляю, PHP работает под PHP-FPM за nginx, воркеры очередей — под supervisor, а планировщик — через cron.

Вопросы и ответы

Q1Как PHP-FPM обрабатывает запрос и почему потом PHP «всё забывает»?

nginx принимает соединение и пересылает PHP-запросы по FastCGI в PHP-FPM. Мастер-процесс FPM держит пул воркеров; свободный воркер выполняет входной скрипт, собирает ответ и возвращает его. Когда запрос завершается, PHP освобождает все созданные объекты и переменные. Эта модель share-nothing — фича: утечки памяти и устаревшее состояние не могут накапливаться, а падение затрагивает только один запрос. Цена — загрузка фреймворка на каждый запрос, которую OPcache сокращает, кэшируя скомпилированный байткод. Постоянное состояние живёт вне PHP — в базе данных, Redis или хранилище сессий. Рантаймы с worker mode вроде Octane или FrankenPHP меняют эту безопасность на скорость.

Q2Что изменилось в PHP 8 и сделало его «современным» языком?

Несколько вещей сразу. Типы стали по-настоящему полезными: union, nullable, mixed, never, типизированные свойства и константы классов, с принудительной проверкой через strict_types. Язык получил выразительные возможности: match-выражения, именованные аргументы, constructor promotion, nullsafe-оператор, enum'ы в 8.1, readonly-свойства и классы, property hooks и асимметричную видимость в 8.4 и pipe-оператор в 8.5. Атрибуты заменили аннотации в docblock'ах для метаданных. Производительность выросла благодаря JIT-компилятору и постоянной работе над движком. Вокруг языка Composer, стандарты PSR, PHPStan и Rector дают набор инструментов, сравнимый с TypeScript. PHP из туториалов 2010 года и сегодняшний PHP — это очень разные языки для работы.

Q3Чем match отличается от switch?

match — это выражение, поэтому он возвращает значение, которое можно сразу присвоить. Он сравнивает по строгой идентичности (===), поэтому '1' не совпадает с 1, что избавляет от целого класса багов switch. Каждая ветка — одно выражение без проваливания, поэтому break невозможно забыть. И главное: если ни одна ветка не совпала и нет default, match бросает UnhandledMatchError, а не молча ничего не делает. В сочетании с enum'ами это значит, что добавленный и забытый новый case громко падает в тестах. switch я всё ещё изредка использую — для веток из нескольких операторов с побочными эффектами, — но для сопоставления значений по умолчанию выбираю match.

Q4Зачем коммитить composer.lock и чем install отличается от update?

composer.json объявляет допустимые диапазоны версий; composer.lock фиксирует точные версии, которые были выбраны, и их хэши. composer install читает lock-файл и устанавливает ровно эти версии, поэтому каждый разработчик, каждый запуск CI и каждый сервер получают идентичный код. composer update игнорирует lock, выбирает самые новые версии, разрешённые диапазонами, и переписывает lock-файл. Поэтому update вы запускаете осознанно, в идеале для одного пакета за раз, просматриваете изменения и коммитите новый lock; деплои и CI всегда выполняют только install. Без закоммиченного lock-файла два сервера, задеплоенные с разницей в неделю, могут работать на разных версиях библиотек.

Q5Как уберечь PHP-кодовую базу от ошибок типов без компилятора?

Многоуровневыми проверками. Во-первых, declare(strict_types=1) и полные объявления типов заставляют PHP бросать TypeError вместо молчаливого приведения значений. Во-вторых, статический анализ через PHPStan или Psalm на высоком уровне в CI читает всю кодовую базу и находит неверные типы, несуществующие методы и невозможные условия ещё до деплоя. Дженерики в docblock'ах (array<int, Order>, Collection<User>) дают анализатору ещё больше информации. В-третьих, enum'ы и readonly value objects затрудняют выражение невалидных состояний. Наконец, тесты покрывают поведение, которое типы описать не могут. Вместе это очень близко к гарантиям компилируемого языка.

Вопросы уровня senior

S1Когда вы перевели бы PHP-приложение на рантайм в worker-режиме вроде Octane или FrankenPHP и какие тут риски?

Я рассматриваю это, когда профилирование показывает, что большая доля времени ответа приходится на бутстрап фреймворка, а не на базу данных или внешние API, и когда трафик достаточно высок, чтобы дополнительная пропускная способность имела значение, — например, нагруженный API с короткими запросами. В таком случае ускорение в несколько раз вполне реально. Риски связаны с состоянием: статические свойства, синглтоны с текущим пользователем или тенантом и кэши в памяти теперь переживают запросы, что может приводить к утечке данных между клиентами или росту памяти, пока процесс не упадёт. Некоторые пакеты для этого просто не написаны. Поэтому перед переходом я провожу аудит биндингов контейнера, помечаю сервисы уровня запроса как scoped, добавляю лимит max-requests и запускаю параллельные нагрузочные тесты с разными пользователями. Если в запросах доминирует медленный SQL, исправление запросов даёт больше пользы при гораздо меньшем риске.

S2Как вы подбираете размер и настраиваете PHP-FPM для сервера и какой режим pm выбираете?

Я начинаю с памяти, а не с догадок. Измерьте средний resident memory воркера под реальным трафиком, вычтите то, что нужно ОС, базе данных, Redis и другим сервисам, и поделите — это потолок для pm.max_children. Для выделенного app-сервера со стабильным трафиком pm = static избавляет от затрат на fork и даёт предсказуемое потребление памяти. Для сервера с несколькими сайтами pm = dynamic или ondemand экономят память для тихих пулов ценой порождения воркеров при всплесках. Я задаю pm.max_requests, чтобы ограничить медленные утечки, включаю pm.status_path, чтобы следить за listen queue и max children reached, и через slow log нахожу запросы, которые слишком долго держат воркеры. Если воркеры в основном ждут I/O, решение обычно — очереди или кэширование, а не пул побольше.

S3Как бы вы спроектировали PHP-кодовую базу, чтобы она пережила много обновлений PHP и фреймворка?

Держать код, который меняется по бизнес-причинам, отдельно от кода, который меняется по техническим. Доменная логика живёт в простых типизированных PHP-классах, value object'ах и enum'ах с минимумом обращений к фреймворку, так что обновление фреймворка затрагивает в основном контроллеры, конфигурацию и инфраструктурные адаптеры. Зависимостей немного, и они выбраны с оглядкой на историю сопровождения, потому что заброшенный пакет часто и есть настоящий блокер обновления. Статический анализ на высоком уровне и автотесты вокруг критичных сценариев делают безопасными крупные автоматические изменения, а Rector механически применяет большинство миграций синтаксиса. CI заранее прогоняется на следующей версии PHP, так что deprecation'ы всплывают за месяцы до того, как станут ошибками. Компромисс — чуть больше структуры на старте, и это окупается при выходе первой же мажорной версии.

Глоссарий

Опкод
Низкоуровневая инструкция для виртуальной машины PHP, получаемая при компиляции исходного файла.
OPcache
Кэш скомпилированных опкодов в разделяемой памяти, чтобы PHP не перекомпилировал файлы на каждый запрос.
Preloading
Однократная загрузка выбранных классов в память при старте сервера, чтобы каждый запрос мог сразу ими пользоваться.
JIT
Just-in-time компилятор, превращающий часто выполняемые опкоды в нативный машинный код; больше всего помогает CPU-bound задачам.
Share-nothing
Каждый запрос начинается с чистой памяти, и ничего, кроме внешнего хранилища, не переходит в следующий.
Worker-режим
Запуск загруженного приложения в долгоживущем процессе, который обслуживает много запросов, как это делают Octane и FrankenPHP.
Fiber
Приостанавливаемый и возобновляемый блок кода (PHP 8.1), на котором асинхронные библиотеки строят event loop'ы.
Object injection
Атака, при которой специально сформированный сериализованный ввод заставляет unserialize создать объекты, чьи магические методы выполняют вредоносный код.
Фиксация сессии
Атакующий заранее задаёт жертве id сессии до логина; перегенерация id после логина это предотвращает.
Listen queue
Запросы, ожидающие свободного воркера FPM; растущая очередь означает, что пул слишком мал или слишком медленный.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.