Глубокая подготовка по программированию на PHP 8.0

Курс для углублённого освоения PHP 8.0: от синтаксиса и современных возможностей языка до объектно-ориентированного проектирования и разработки веб‑приложений. Рассматриваются практики качества кода, тестирование, работа с базами данных, безопасность и основы производительности.

1. Основы PHP 8.0 и современные возможности языка

Основы PHP 8.0 и современные возможности языка

Роль PHP 8.0 в современной веб-разработке

PHP — серверный язык программирования, который чаще всего используют для создания веб-приложений и API. Код PHP исполняется на сервере, а клиент (браузер или мобильное приложение) получает уже готовый результат: HTML, JSON и т.д.

PHP 8.0 — крупный релиз, который усилил типизацию, добавил новые конструкции языка и улучшил производительность (в том числе за счёт JIT). В этом курсе мы будем писать код так, как принято в современной PHP-разработке: с понятной структурой, типами, предсказуемым поведением и ясной обработкой ошибок.

!Схема показывает, где именно исполняется PHP-код и как запрос проходит через инфраструктуру

Запуск PHP и базовая структура программы

Способы запуска

* Через веб-сервер (типичный продакшен): Nginx/Apache + PHP-FPM. * Через встроенный сервер для разработки: php -S localhost:8000 -t public. * Через CLI (консольные скрипты): php script.php.

Официальная документация по CLI:

* PHP Command line usage

Минимальный пример

Ключевые элементы:

* <?php — начало PHP-кода. * declare(strict_types=1); — включает строгую проверку типов для аргументов и возвращаемых значений в текущем файле. * echo — вывод строки.

Документация:

* declare

Переменные, типы и строгая типизация

Переменные и базовые типы

Переменные в PHP начинаются с a, int a + userId): ?string { // Если не найдено — возвращаем null return null; } php config['limit'] ?? 100; php user?->getProfile()?->getEmail();

Документация:

* Named arguments

Стрелочные функции

Стрелочные функции (fn) удобны для коротких колбэков и автоматически захватывают переменные из внешней области видимости по значению.

Документация:

* match

Ошибки и исключения

Ошибки vs исключения

В современном PHP-коде предпочтительно обрабатывать проблемные ситуации через исключения.

Promotion свойств конструктора (PHP 8.0)

PHP 8.0 позволяет объявлять и инициализировать свойства прямо в параметрах конструктора.

Документация:

* Namespaces

Composer и PSR-4 (почему это важно)

В реальных проектах автозагрузка классов обычно настраивается через Composer и стандарт PSR-4. Это позволяет не писать вручную require/include для каждого файла.

Документация:

* Composer

Система типов PHP 8.0: union types и mixed

Union types (PHP 8.0)

Если функция может принимать несколько разных типов, можно указать объединение через |.

Это лучше, чем оставлять тип неуказанным, потому что контракт становится явным.

Документация:

* Union Types

mixed

mixed означает «может быть что угодно». Это крайний случай: он полезен на границе системы (например, входные данные), но внутри доменной логики лучше стремиться к конкретным типам.

Документация:

* mixed

Атрибуты (PHP 8.0)

Атрибуты — встроенный способ добавлять метаданные к классам, методам, свойствам, параметрам. Это замена части сценариев, где раньше использовали docblock-аннотации.

Пример: пометим метод как устаревший (демонстрация идеи).

Атрибуты читаются через Reflection (к этому мы подойдём позже в курсе, когда будем разбирать метапрограммирование и инструменты фреймворков).

Документация:

* Attributes

Производительность и JIT

PHP 8.0 включает JIT (Just-In-Time compiler) как часть движка OPcache. Важно понимать практический вывод:

* JIT не всегда ускоряет типичные веб-запросы так сильно, как вычислительные задачи. * Главные выигрыши в вебе часто дают архитектура, кэширование, оптимизация запросов в БД и снижение I/O.

Тем не менее, сам факт появления JIT — показатель движения PHP в сторону более высокой производительности.

Документация:

* OPcache

Рекомендуемый стиль кода и минимальные правила качества

Чтобы код был поддерживаемым, с первого занятия закрепим базовые правила:

* Включайте declare(strict_types=1); в файлах с бизнес-логикой. * Всегда указывайте типы аргументов и возвращаемых значений, где это разумно. * Избегайте «магических» значений: используйте константы, enum (появится в PHP 8.1) или отдельные типы. * Предпочитайте исключения молчаливым ошибкам. * Разделяйте ответственность: функции и классы должны делать одну понятную вещь.

Итоги

В этой статье вы:

* разобрали, как запускается PHP и зачем нужен strict_types * закрепили базовые типы и работу с null * познакомились с ключевыми возможностями PHP 8.0: match, именованные аргументы, union types, nullsafe operator, promotion свойств конструктора, атрибуты, throw` как выражение * увидели основу ООП, namespaces и роль Composer

В следующих материалах курса мы будем углублять эти основы: детально разберём массивы и строки, работу с датой/временем, I/O, затем перейдём к архитектуре приложений, тестированию и практикам промышленной разработки на PHP 8.

2. Типизация, исключения и работа с ошибками

Типизация, исключения и работа с ошибками

Как эта тема связана с предыдущим материалом

В предыдущей статье мы включили declare(strict_types=1);, познакомились с базовыми типами, match, throw как выражением и общими принципами современного PHP 8.0. Теперь углубимся в две ключевые практики промышленной разработки:

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

    Типизация в PHP 8.0 как контракт

    Тип в современном PHP стоит воспринимать как контракт между тем, кто вызывает функцию или метод, и тем, кто её реализует.

    Если контракт нарушен, PHP 8.0 чаще всего прерывает выполнение и выбрасывает TypeError. Это хорошо: проблема обнаруживается сразу, а не превращается в неявный баг.

    Документация:

  • Объявления типов
  • strict_types и границы строгой типизации

    declare(strict_types=1); влияет на то, как PHP проверяет типы аргументов и возвращаемых значений в текущем файле.

  • В строгом режиме PHP не делает «удобных» приведений типов при вызове функций.
  • В нестрогом режиме PHP может попытаться привести скалярные типы (например, строку к числу), что нередко скрывает ошибки.
  • Пример с аргументами:

    Важно понимать границу:

  • strict_types относится к вызову функции (к месту, где вы её вызываете), а не к месту, где она объявлена.
  • поэтому в проекте обычно договариваются включать strict_types=1 везде (или хотя бы во всех файлах бизнес-логики)
  • Документация:

  • declare
  • Скаляры и типовые ловушки

    PHP имеет скалярные типы int, float, string, bool. Важно отличать:

  • валидное значение нужного типа
  • значение, которое можно привести
  • значение, которое выглядит похоже, но является ошибкой
  • Пример: пользовательский ввод почти всегда строка.

    Практическое правило:

  • на границе системы (HTTP-запрос, CLI-аргументы, чтение файлов) допускаются приведения и проверки
  • внутри приложения лучше передавать уже типизированные значения
  • Возвращаемые типы void и типизация результата

    Если метод ничего не возвращает, используйте : void. Это делает API класса яснее.

    Тип возвращаемого значения особенно важен, потому что:

  • ускоряет чтение кода
  • упрощает рефакторинг
  • улучшает качество статического анализа
  • Документация:

  • Возвращаемые типы
  • Nullable-типы и null как часть контракта

    Если null допустим, это должно быть отражено в типе.

    Документация:

  • iterable
  • callable
  • Ошибки типизации на практике

    При нарушении контракта вы чаще всего увидите:

  • TypeError при неверном типе аргумента или возвращаемого значения
  • ArgumentCountError при неверном количестве аргументов
  • ValueError в случаях, когда тип правильный, но значение недопустимо (в PHP 8.0 многие внутренние функции стали выбрасывать ValueError)
  • Документация:

  • TypeError
  • ArgumentCountError
  • ValueError
  • Исключения как инструмент управления ошибками

    Исключение — это способ сообщить о проблеме так, чтобы вызывающий код:

  • мог обработать ситуацию
  • мог добавить контекст
  • мог принять решение: повторить, вернуть ошибку пользователю, залогировать, прекратить выполнение
  • Документация:

  • Исключения
  • Throwable: общее для Exception и Error

    В PHP 8.0 и Exception, и Error реализуют интерфейс Throwable. Это означает, что в catch можно перехватывать как прикладные исключения, так и многие ошибки движка.

    В PHP 8.0 throw стал выражением, что удобно для компактной валидации:

    Важно:

  • используйте это там, где выражение действительно повышает читабельность
  • не превращайте код в «головоломку из однострочников»
  • try/catch/finally и управление ресурсами

    Базовый шаблон:

    finally полезен для освобождения ресурсов, даже если что-то пошло не так:

  • закрыть файл
  • вернуть соединение в пул
  • завершить транзакцию (хотя для транзакций обычно используют более специализированные конструкции)
  • Документация:

  • try/catch
  • Создание своих исключений

    Собственные исключения помогают:

  • отделять ошибки вашего домена от низкоуровневых (PDOException, сетевых ошибок)
  • централизованно обрабатывать разные категории проблем
  • Важно:

  • это мощный инструмент, но применять его нужно осознанно, чтобы не «сломать» поведение сторонних библиотек
  • Документация:

  • set_error_handler
  • ErrorException
  • Практическая стратегия: где проверять типы, а где ловить исключения

    Удобная модель для приложения:

  • на входе: парсинг и валидация входных данных, приведение типов
  • в середине: доменная логика с жёсткими контрактами типов и понятными исключениями
  • на выходе: преобразование исключений в понятный ответ (HTTP, CLI) и логирование
  • !Диаграмма показывает, где выполнять приведение типов и как исключения проходят через слои приложения

    Ключевые правила:

  • не доверяйте входным данным, проверяйте их до вызова бизнес-логики
  • исключения должны быть осмысленными и помогать понять, что пошло не так
  • не используйте исключения как обычную ветвистую логику (например, для «нормального» перебора вариантов)
  • разделяйте: логирование, обработка, и формирование ответа пользователю
  • Итоги

    В этой статье вы:

  • разобрали, как типизация превращает код в набор проверяемых контрактов
  • поняли роль declare(strict_types=1); и почему важно быть последовательным
  • научились различать Exception, Error и общий интерфейс Throwable
  • закрепили шаблоны try/catch/finally, создание собственных исключений и оборачивание с сохранением причины
  • увидели, как настраивать репортинг и логирование ошибок, а также как при необходимости превращать предупреждения в исключения
  • Дальше в курсе эти принципы будут постоянно использоваться: в работе со строками и массивами, при построении архитектуры, в тестировании и при интеграции с инфраструктурой.

    3. ООП в PHP: классы, интерфейсы, трейты и SOLID

    ООП в PHP: классы, интерфейсы, трейты и SOLID

    Зачем ООП в курсе про PHP 8.0

    В предыдущих материалах мы опирались на типизацию как на контракт и на исключения как на управляемый способ сообщать об ошибках. ООП в PHP 8.0 продолжает эту линию: мы строим код из компонентов с ясными обязанностями, явными зависимостями и предсказуемым поведением.

    ООП особенно полезно, когда проект растёт:

  • появляется несколько источников данных (БД, внешние API, кэш)
  • усложняется бизнес-логика
  • растёт команда и важна поддерживаемость
  • Документация:

  • Классы и объекты в PHP
  • Базовые понятия ООП без «магии»

    В контексте PHP 8.0 нам нужны четыре идеи.

  • Инкапсуляция — прятать детали реализации и давать безопасный публичный API.
  • Абстракция — выделять главное и скрывать второстепенное.
  • Полиморфизм — работать с объектами через общий контракт (обычно интерфейс), не зная конкретный класс.
  • Наследование — переиспользовать и расширять поведение, но применять осознанно.
  • Почти всегда в прикладном коде выигрывает связка: композиция + интерфейсы + явные исключения.

    Классы в PHP 8.0 как носители инвариантов

    Класс — это не просто набор методов. В хорошей модели класс отвечает за инварианты — правила корректности состояния.

    Пример: объект Email гарантирует, что внутри него всегда валидный email. Тогда остальной код может доверять типу Email и не повторять проверки.

    Ключевая выгода интерфейсов:

  • код зависит от абстракции, а не от конкретного класса
  • проще тестировать (можно подменить реализацию)
  • проще расширять систему, добавляя новые реализации
  • Документация:

  • Интерфейсы
  • Throwable
  • Композиция и внедрение зависимостей через интерфейсы

    Рассмотрим класс, которому нужно отправлять письма. Если он напрямую создаёт конкретный транспорт, он становится жёстко связанным с реализацией.

    Лучше: зависеть от интерфейса.

    Если два трейта добавляют одинаковый метод, конфликт решается явно.

    Документация:

  • Traits
  • Когда выбирать класс, интерфейс или трейт

    В прикладном коде помогает простая «шпаргалка».

  • Класс — когда нужно хранить состояние, защищать инварианты, иметь реализацию и публичный API.
  • Интерфейс — когда нужен контракт и полиморфизм, чтобы подменять реализацию.
  • Трейт — когда нужно переиспользовать небольшой кусок реализации, не создавая жёсткую иерархию.
  • Если сомневаетесь, начните с интерфейса + композиции. Трейты подключайте только при явной повторяемости кода.

    SOLID в контексте PHP 8.0

    SOLID — это набор принципов проектирования, которые помогают снижать связанность и делать код расширяемым. Здесь важно не «заучить аббревиатуру», а понимать практический смысл.

    Источник:

  • SOLID (обзор принципов)
  • Single Responsibility Principle

    Один модуль — одна причина для изменения.

    Плохой признак: класс одновременно валидирует входные данные, пишет в БД, отправляет письма и логирует.

    Хорошая практика:

  • разделять операции на отдельные классы (валидатор, репозиторий, сервис отправки)
  • собирать их через композицию
  • Open-Closed Principle

    Сущности должны быть открыты для расширения, но закрыты для модификации.

    Типичный путь в PHP: добавлять новые реализации интерфейса, не трогая код, который от интерфейса зависит.

    Документация:

  • PDOException
  • Практические рекомендации по дизайну в стиле PHP 8.0

  • Включайте declare(strict_types=1); в файлах бизнес-логики.
  • Делайте публичный API классов минимальным, свойства держите private.
  • Используйте интерфейсы для зависимостей и полиморфизма.
  • Предпочитайте композицию наследованию.
  • Трейты применяйте точечно и держите их маленькими.
  • Ошибки домена оформляйте собственными исключениями, инфраструктурные сбои оборачивайте, сохраняя причину.
  • Итоги

    В этой статье вы:

  • закрепили роль классов как носителей инвариантов и контрактов
  • научились различать задачи классов, интерфейсов и трейтов
  • увидели, как композиция и интерфейсы улучшают тестируемость и расширяемость
  • разобрали SOLID на практических примерах под PHP 8.0
  • связали ООП с типизацией и исключениями из предыдущих тем курса
  • 4. Работа с данными: PDO, MySQL, транзакции и миграции

    Работа с данными: PDO, MySQL, транзакции и миграции

    Как эта тема связана с предыдущими статьями

    Ранее в курсе мы закрепили три фундаментальные опоры современного PHP 8.0:

  • типизация как контракт (declare(strict_types=1);, типы аргументов и результатов)
  • исключения как управляемый способ сообщать о проблемах (Throwable, оборачивание исключений)
  • ООП и SOLID как способ строить поддерживаемые компоненты (интерфейсы, композиция, зависимость от абстракций)
  • Работа с базой данных объединяет все эти практики: мы будем получать данные на границе системы, валидировать и типизировать их, выполнять запросы безопасно (без SQL-инъекций), а ошибки инфраструктуры превращать в понятные исключения домена.

    !Схема показывает, где именно находится PDO-слой и как через него проходят данные и ошибки

    MySQL и подключение через PDO

    Почему PDO

    PDO (PHP Data Objects) — стандартный слой доступа к данным в PHP, который:

  • даёт единый API для разных СУБД
  • поддерживает подготовленные выражения (prepared statements)
  • хорошо сочетается с исключениями (PDOException) и строгой типизацией
  • Документация:

  • PDO
  • DSN и создание подключения

    Для MySQL чаще всего используется драйвер pdo_mysql и DSN вида mysql:host=...;dbname=...;charset=....

    Критически важные настройки PDO для промышленного кода:

  • включить исключения при ошибках (PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
  • задать режим выборки по умолчанию (PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC)
  • отключить эмуляцию prepared statements (часто рекомендуют PDO::ATTR_EMULATE_PREPARES => false)
  • Документация:

  • PDO::setAttribute
  • Подключение к MySQL через PDO
  • Пример подключения:

    Практическое правило: храните DSN и пароль вне кода (переменные окружения, секреты CI/CD), а в репозиторий не коммитьте реальные доступы.

    Безопасные запросы: подготовленные выражения

    Почему нельзя собирать SQL строкой

    SQL-инъекция появляется, когда вы вставляете пользовательский ввод прямо в SQL:

  • нельзя делать "... WHERE email = 'pdo) {}
  • public function findIdByEmail(string stmt = stmt->execute(['email' => row = row === false) { return null; }

    return (int)row['id'] делается на границе между БД (строки) и доменной логикой (строгие типы)

    INSERT и получение lastInsertId

    Польза: вы снижаете риск неочевидных преобразований и делаете намерение кода явным.

    Чтение данных: fetch modes и форма результата

    По умолчанию мы выставили PDO::FETCH_ASSOC, поэтому fetch() и fetchAll() возвращают массивы, где ключи — имена колонок.

    Документация:

  • PDOStatement::fetch
  • Практические рекомендации:

  • возвращайте из репозиториев либо простые скаляры (?int, ?string), либо небольшие DTO/Value Object
  • не протаскивайте «сырые» ассоциативные массивы глубоко в доменную логику, иначе типизация теряется
  • Ошибки БД и стратегия исключений

    Что именно бросает PDO

    Если включён PDO::ERRMODE_EXCEPTION, ошибки запросов и соединения обычно превращаются в PDOException.

    Документация:

  • PDOException
  • Оборачивание инфраструктурных ошибок в прикладные

    Свяжем это с SOLID и принципом инверсии зависимостей: бизнес-слой не должен зависеть от деталей PDO. Часто удобнее поймать PDOException и выбросить исключение своего слоя.

    php <?php

    declare(strict_types=1);

    final class RegistrationService { public function __construct( private PDO email): int { stmt = stmt->execute(['email' => userId = (int)stmt = stmt->execute(['uid' => this->pdo->commit();

    return e) { if (this->pdo->rollBack(); }

    throw value) { value); if (value, FILTER_VALIDATE_EMAIL) === false) { throw new InvalidArgumentException('Invalid email'); } value; }

    public function asString(): string { return id, private Email this->id; } public function email(): Email { return id): User; }

    final class UserNotFoundException extends RuntimeException {} final class UserStorageException extends RuntimeException {}

    final class PdoUserRepository implements UserRepository { public function __construct(private PDO id): User { try { this->pdo->prepare('SELECT id, email FROM users WHERE id = :id'); id]); stmt->fetch();

    if (id not found"); }

    return new User( (int)row['email']) ); } catch (UserNotFoundException e; } catch (PDOException e); } } } ``

    Здесь видно ключевое:

  • на выходе из репозитория вы получаете типизированный User, а не массив
  • инфраструктурные детали (PDOException) не «протекают» вверх
  • инварианты (Email) защищают доменную модель от некорректных данных
  • Итоги

    В этой статье вы:

  • настроили PDO-подключение к MySQL в стиле PHP 8.0 (исключения, fetch mode, запрет эмуляции)
  • научились писать безопасные запросы через prepared statements
  • разобрали, как типизировать границу между БД и доменной логикой
  • освоили практический шаблон транзакций с beginTransaction/commit/rollBack`
  • поняли, зачем нужны миграции и как они поддерживают воспроизводимость схемы
  • Дальше эти навыки станут базой для тем про архитектуру приложений, тестирование (в том числе репозиториев через подмену зависимостей) и интеграцию с инфраструктурой.

    5. Веб‑разработка: HTTP, маршрутизация, формы и сессии

    Веб‑разработка: HTTP, маршрутизация, формы и сессии

    Связь с предыдущими темами курса

    Ранее мы уже построили фундамент «промышленного» PHP 8.0:

  • типизация как контракт (declare(strict_types=1);, TypeError, явные типы)
  • исключения как управляемый механизм ошибок (Throwable, оборачивание)
  • ООП и SOLID как способ собирать приложение из компонентов (интерфейсы, композиция)
  • работа с данными через PDO как безопасная граница к БД (prepared statements, транзакции)
  • Веб‑разработка соединяет эти темы. HTTP‑запрос приносит сырой ввод (строки, заголовки, тело), который нужно:

  • распарсить и валидировать на границе системы
  • передать в типизированную доменную логику
  • корректно обработать ошибки и вернуть HTTP‑ответ
  • !Общая картина: как запрос проходит через фронт‑контроллер, роутер и обработчики до ответа

    HTTP как основа веб‑приложений

    HTTP — протокол, через который клиент (браузер, мобильное приложение) общается с сервером.

    > “HTTP is a stateless request/response protocol.” — RFC 9110 > > Источник: RFC 9110: HTTP Semantics

    Stateless означает: каждый запрос самодостаточен, и сервер не обязан «помнить» прошлые запросы. Сессии и куки — это надстройки, которые помогают реализовать «состояние поверх stateless».

    Из чего состоит HTTP‑запрос

    Обычно полезно мыслить так:

  • Метод: GET, POST, PUT, PATCH, DELETE.
  • Путь: например /login.
  • Query string: часть после ?, например /users?id=10.
  • Заголовки: метаданные, например Accept, Content-Type, Cookie.
  • Тело: данные формы или JSON (обычно есть у POST/PUT/PATCH).
  • В PHP многие части запроса доступны через суперглобальные массивы:

  • _GET — query‑параметры
  • _COOKIE — куки
  • Документация:

  • PHP: Superglobals
  • Из чего состоит HTTP‑ответ

    Ответ, который сервер возвращает клиенту, включает:

  • статус‑код (например, 200, 302, 404)
  • заголовки (например, Content-Type, Location, Set-Cookie)
  • тело (HTML, JSON и т.д.)
  • В PHP заголовки и статус обычно задают функцией header().

    Документация:

  • PHP: header
  • Методы, «безопасность» и идемпотентность

    На практике важно понимать смысл методов:

  • GET — получение данных; не должен менять состояние на сервере.
  • POST — создание/действие; меняет состояние.
  • PUT — полная замена ресурса.
  • PATCH — частичное изменение ресурса.
  • DELETE — удаление.
  • В типичном PHP‑приложении без сложного API чаще всего используются GET и POST, но роутер почти всегда стоит проектировать так, чтобы он учитывал метод.

    Справка:

  • MDN: HTTP request methods
  • Статус‑коды, которые вы будете видеть постоянно

    | Код | Смысл | Когда применять | | --- | --- | --- | | 200 | OK | успешный ответ с телом | | 201 | Created | создан ресурс (часто в API) | | 302 | Found | редирект после POST (часто) | | 303 | See Other | редирект после POST на GET (явный PRG) | | 400 | Bad Request | неверный формат/параметры запроса | | 401 | Unauthorized | не аутентифицирован | | 403 | Forbidden | нет прав | | 404 | Not Found | маршрут/ресурс не найден | | 422 | Unprocessable Content | валидация не прошла | | 500 | Internal Server Error | непредвиденная ошибка |

    Справка:

  • MDN: HTTP response status codes
  • Front Controller и маршрутизация

    Почему почти все PHP‑приложения используют front controller

    Front controller — это один входной файл, который принимает все запросы и решает, что делать дальше. Обычно это public/index.php.

    Плюсы подхода:

  • единая точка для обработки ошибок и логирования
  • централизованная маршрутизация
  • проще подключать middleware (аутентификация, CSRF, метрики)
  • Минимальная структура проекта

    Типичная раскладка:

  • public/index.php — входная точка
  • src/ — классы приложения
  • templates/ — HTML‑шаблоны
  • Объекты Request и Response как типизированная граница

    Как и в теме про PDO, ключевая идея: на границе системы мы приводим данные к типам и нормализуем, а внутрь доменной логики отправляем уже предсказуемые значения.

    Ниже — упрощённые классы Request и Response.

    Мы используем match из PHP 8.0 и исключения, чтобы:

  • отделить «маршрут не найден» (404) от «метод не поддержан» (405)
  • не возвращать null как «ошибку маршрутизации»
  • Front controller с централизованной обработкой ошибок

    Теперь соберём public/index.php.

    php <?php

    declare(strict_types=1);

    function csrfToken(): string { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); }

    _SESSION['csrf'] ?? null; if (!is_string(token === '') { _SESSION['csrf'] = token; }

    function assertCsrfToken(mixed expected = expected) || !is_string(expected, _SESSION — массив, который PHP загружает и сохраняет автоматически при session_start()

    Документация:

  • PHP: Sessions
  • Базовое использование сессии

    Документация:

  • PHP: session_regenerate_id
  • Flash‑сообщения через сессию

    Flash — сообщение «показать один раз после редиректа». Это идеально сочетается с PRG.

    `php <?php

    declare(strict_types=1);

    function flashSet(string value): void { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } key] = key): ?string { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); }

    _SESSION['flash'][_SESSION['flash'][value) ? router->get('/login', function (Request token = csrfToken(); html = ''; if (html .= '<p>' . e(html .= '<form method="POST" action="/login">' . '<input type="hidden" name="csrf" value="' . e(html); });

    r): Response { try { assertCsrfToken(email = requireNonEmptyString(email = requireEmail(_SESSION['user_email'] = e) { flashSet('error', router->get('/profile', function (Request email = email)) { return Response::redirect('/login'); }

    return Response::html('<h1>Profile</h1><p>' . e(_GET/_SERVER на границе (Request/контроллер), а внутрь передавайте типы.

  • Роутер должен учитывать и path, и method.
  • Для форм используйте PRG: после успешного POST делайте редирект.
  • Всегда экранируйте вывод пользовательских данных (htmlspecialchars).
  • Для любых state‑changing форм добавляйте CSRF‑токен.
  • После логина вызывайте session_regenerate_id(true).
  • В продакшене не показывайте детали ошибок пользователю, но логируйте исключения.
  • Итоги

    В этой статье вы построили связную модель веб‑приложения на PHP 8.0:

  • поняли структуру HTTP‑запроса и ответа, методы и статус‑коды
  • разобрали front controller и базовую маршрутизацию method + path
  • научились обрабатывать формы с нормализацией/валидацией и защищать вывод от XSS
  • освоили PRG и flash‑сообщения
  • разобрали сессии и куки, CSRF‑защиту и меры против session fixation
  • Эти знания станут основой для следующего уровня: архитектуры веб‑приложений (слои, контроллеры, сервисы), тестирования обработчиков и более строгой модели запросов/ответов.

    6. Безопасность веб‑приложений на PHP

    Безопасность веб‑приложений на PHP

    Как тема связана с предыдущими статьями

    В прошлых материалах курса мы уже построили каркас современного PHP 8.0 приложения:

  • типизация и declare(strict_types=1); как способ сделать поведение предсказуемым
  • исключения как управляемая модель ошибок (Throwable, оборачивание)
  • ООП и SOLID для разделения ответственности
  • PDO и prepared statements как безопасная работа с БД
  • HTTP, роутинг, формы, CSRF и сессии как базовая веб‑модель
  • Безопасность соединяет всё это в одну практику: мы не просто «закрываем дыры», а проектируем приложение так, чтобы опасные данные оставались на границе, а внутри работали типизированные объекты, понятные исключения и ограниченные привилегии.

    !Схема показывает, где именно должны происходить проверки безопасности и как запрос проходит через слои

    Модель угроз: что именно мы защищаем

    Вместо списка «страшных слов» полезно держать в голове простую модель.

  • Активы: данные пользователей, деньги/заказы, учётные записи, доступы к интеграциям.
  • Границы доверия: браузер, HTTP‑запрос, внешние API, файловая система, БД.
  • Типовые атакующие: любой пользователь интернета, пользователь с чужой сессией, внутренний пользователь с недостаточными правами.
  • Хороший практический ориентир для веб‑приложений — проект OWASP Top 10.

    Ссылка:

  • OWASP Top 10
  • Базовое правило безопасности

  • Никогда не доверяйте входным данным: всё, что приходит из _POST, value, string value)) {
  • throw new ValidationException("Field 'value = trim(value === '') { throw new ValidationException("Field 'value; }

    function requireIntId(mixed field): int { if (is_int(value; }

    if (!is_string(value)) { throw new ValidationException("Field 'value; } php <?php

    declare(strict_types=1);

    final class UserReadModel { public function __construct(private PDO email): ?array { this->pdo->prepare( 'SELECT id, email FROM users WHERE email = :email LIMIT 1' );

    email]);

    stmt->fetch(); return row; } } php <?php

    declare(strict_types=1);

    pdo->prepare('SELECT * FROM orders WHERE user_id = :uid LIMIT :lim'); userId, PDO::PARAM_INT); limit, PDO::PARAM_INT); rows = value): string { return htmlspecialchars(hash = password_hash(hash === false) { throw new RuntimeException('Failed to hash password'); }

    if (!password_verify(hashFromDb)) { throw new RuntimeException('Invalid credentials'); } php <?php

    declare(strict_types=1);

    final class ForbiddenException extends RuntimeException {}

    function assertCanEditProfile(int profileOwnerId): void { if (profileOwnerId) { throw new ForbiddenException('Access denied'); } } php <?php

    declare(strict_types=1);

    session_set_cookie_params([ 'httponly' => true, 'secure' => true, 'samesite' => 'Lax', ]);

    session_start(); php <?php

    declare(strict_types=1);

    final class Response { public function __construct( public int headers, public string name, string clone = clone clone->headers[value; return response = (new Response(200, ['Content-Type' => 'text/html; charset=utf-8'], 'ok')) ->withHeader('X-Content-Type-Options', 'nosniff') ->withHeader('X-Frame-Options', 'DENY') ->withHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); php <?php

    declare(strict_types=1);

    final class UploadException extends RuntimeException {}

    function saveUploadedImage(array dir): string { if ((tmp = tmp) || tmp)) { throw new UploadException('Invalid upload source'); }

    file['size'] ?? 0; if (!is_int(size <= 0 || finfo = new finfo(FILEINFO_MIME_TYPE); finfo->file(allowed = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', ];

    if (!is_string(allowed[name = bin2hex(random_bytes(16)) . '.' . mime]; dir, '/\\') . DIRECTORY_SEPARATOR . tmp, name; } `

    Документация:

  • is_uploaded_file
  • move_uploaded_file
  • finfo
  • SSRF, path traversal и инъекции команд

    Эти проблемы часто возникают, когда приложение «удобно» проксирует доступ к ресурсам.

    SSRF

    SSRF появляется, когда пользователь может заставить сервер сделать запрос по произвольному URL.

    Практические меры:

  • не принимать URL напрямую, принимать идентификатор ресурса и выбирать URL из белого списка
  • запрещать приватные адреса и локальные хосты
  • явно ограничивать протоколы и порты
  • Path traversal

    Path traversal появляется, когда пользователь управляет путём к файлу.

    Практические меры:

  • никогда не собирать путь простым конкатенированием пользовательского ввода
  • использовать basename() только как частичную меру, а лучше работать через заранее известные идентификаторы
  • проверять, что итоговый realpath() находится внутри разрешённой директории
  • Документация:

  • realpath
  • basename
  • Command injection

    Если вы используете внешние команды, используйте безопасные API и экранирование.

    Документация:

  • escapeshellarg
  • Практический ориентир: по возможности не вызывать shell вообще.

    Десериализация: не используйте unserialize на недоверенных данных

    unserialize() над недоверенным вводом может приводить к тяжёлым последствиям.

    Безопасный базовый подход для обмена данными:

  • JSON (json_decode) вместо сериализации объектов
  • Документация:

  • unserialize
  • json_decode
  • Ошибки, логирование и утечки информации

    В статье про исключения мы уже разделяли:

  • что показывать пользователю
  • что писать в лог
  • Для безопасности это критично: сообщения об ошибках не должны раскрывать:

  • SQL‑запросы
  • пути к файлам на сервере
  • значения секретов
  • внутреннюю структуру приложения
  • Практический шаблон:

  • пользователю: нейтральное сообщение и корректный HTTP‑код
  • в лог: полный стек и контекст
  • Документация:

  • error_log
  • Управление зависимостями и уязвимости библиотек

    Даже если ваш код идеален, уязвимость может быть в зависимости.

    Практический минимум:

  • фиксировать зависимости через composer.lock
  • регулярно обновлять зависимости
  • использовать composer audit
  • Документация:

  • Composer audit
  • Минимальный чек‑лист для проектов на PHP 8.0

  • Ввод: нормализовать и валидировать на границе, не таскать mixed внутрь.
  • SQL: только prepared statements.
  • HTML: экранировать вывод, не выводить недоверенное напрямую.
  • CSRF: токены для state‑changing форм.
  • Пароли: password_hash и password_verify.
  • Сессии: session_regenerate_id(true) после логина, HttpOnly, Secure, SameSite.
  • HTTPS: включить везде, где есть учётные записи и сессии.
  • Ошибки: пользователю минимум, в лог максимум.
  • Зависимости: обновления и composer audit`.
  • Итоги

    В этой статье вы собрали «каркас безопасности» для веб‑приложений на PHP 8.0:

  • закрепили подход границы доверия: валидация и типизация ввода на входе, строгие контракты внутри
  • разобрали ключевые классы атак: SQL‑инъекции, XSS, CSRF, проблемы с сессиями
  • научились правильно хранить пароли и усиливать аутентификацию
  • узнали базовые меры для файловых загрузок, SSRF, path traversal и опасной десериализации
  • связали безопасность с логированием, обработкой ошибок и управлением зависимостями
  • Дальше эти принципы будут применяться в более «архитектурном» виде: разделение на middleware, сервисы, политики доступа, централизованная обработка ошибок и тестирование сценариев безопасности.

    7. Тестирование, качество кода и оптимизация производительности

    Тестирование, качество кода и оптимизация производительности

    Как эта тема связана с предыдущими статьями

    Ранее в курсе мы выстроили практику современного PHP 8.0:

  • строгая типизация как контракт (declare(strict_types=1);, типы, TypeError)
  • исключения как управляемая модель ошибок (Throwable, свои исключения, оборачивание)
  • ООП и SOLID как способ разделять ответственность и зависеть от абстракций
  • PDO, транзакции и миграции как безопасная и предсказуемая работа с данными
  • HTTP, роутинг, формы, сессии и безопасность как практическая веб-модель
  • Теперь добавим три практики, которые превращают набор файлов в поддерживаемый и предсказуемый продукт:

  • тестирование как защита от регрессий и способ проектировать API
  • качество кода как дисциплина (стиль, статический анализ, архитектурные границы)
  • оптимизация производительности как работа на основе измерений, а не предположений
  • !Пирамида тестирования помогает понять, каких тестов должно быть больше

    Тестирование как часть дизайна

    Тесты в PHP-проекте решают три задачи:

  • фиксируют ожидаемое поведение и защищают от регрессий
  • делают рефакторинг безопасным
  • вынуждают проектировать код с ясными зависимостями и контрактами
  • Важно связать тестирование с тем, что уже было в курсе:

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

    Unit-тесты

    Unit-тест проверяет маленький фрагмент логики в изоляции от инфраструктуры.

    Признаки хорошего unit-теста:

  • быстрый (миллисекунды)
  • не требует БД, сети и файловой системы
  • проверяет один сценарий
  • Идеальные кандидаты из нашего курса:

  • value objects (Email)
  • политики и стратегии (например, DiscountPolicy)
  • сервисы, зависящие от интерфейсов (например, SendWelcomeEmail)
  • Интеграционные тесты

    Интеграционный тест проверяет связку компонентов и реальную инфраструктуру или её близкий аналог.

    Типичные примеры:

  • репозиторий на PDO выполняет запросы и правильно маппит строки БД в объекты
  • транзакция действительно откатывает изменения
  • Важный принцип из предыдущих тем: на границе БД вы приводите типы и оборачиваете PDOException в исключения своего слоя, и это тоже должно быть покрыто тестом.

    E2E-тесты

    End-to-end тест проверяет сценарий как пользователь: от HTTP-запроса до ответа.

    В рамках курса без фреймворка такие тесты можно делать на уровне:

  • запуска приложения встроенным сервером PHP
  • отправки HTTP-запросов снаружи
  • Но практическая рекомендация обычно такая:

  • E2E тестов должно быть мало (они дорогие и нестабильные)
  • основная масса покрытия должна быть unit + интеграция
  • PHPUnit: базовый инструмент тестирования в PHP

    Самый распространённый фреймворк для тестирования в PHP — PHPUnit.

    Документация:

  • PHPUnit Manual
  • Минимальная установка через Composer

    Обычно PHPUnit устанавливают как dev-зависимость:

    Далее вы запускаете тесты:

    Пример unit-тестов: проверяем инварианты и исключения

    Возьмём value object Email из статьи про ООП: он гарантирует валидность и выбрасывает исключение при некорректном значении.

    Класс:

    Unit-тесты:

    Здесь важно:

  • мы тестируем контракт и инвариант объекта
  • ошибка моделируется исключением, поэтому её легко проверять через expectException
  • Тест-дубли: когда и как подменять зависимости

    Когда класс зависит от интерфейса, вы можете подменить реализацию в тесте.

    Термины:

  • stub возвращает заранее заданные значения
  • fake упрощённая, но рабочая реализация (например, in-memory репозиторий)
  • mock проверяет, что метод был вызван определённым образом
  • Практический ориентир:

  • начинайте со stub/fake, мок используйте только когда нужно проверить факт взаимодействия
  • Пример: сервис зависит от Mailer. В тесте мы хотим проверить, что письмо отправлено.

    Мы не привязываемся к конкретной библиотеке отправки писем, а тестируем поведение через интерфейс, как и требует SOLID.

    Интеграционные тесты для PDO: проверяем реальное взаимодействие

    Интеграционные тесты особенно полезны для репозиториев:

  • SQL легко написать «почти правильно», но ошибка проявится только на реальном запросе
  • маппинг типов (строка из БД в int или Email) лучше проверять фактом
  • Практическая идея для тестов:

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

  • запустить БД в отдельном окружении (локально или в CI)
  • выполнить миграции
  • выполнить тест
  • Для транзакционных таблиц InnoDB часто удобно оборачивать каждый тест в транзакцию и делать ROLLBACK в конце.

    Контрактные тесты для интерфейсов

    Если у вас есть интерфейс (например, UserRepository), и несколько реализаций (PDO, in-memory, API), то полезно проверить, что все реализации ведут себя одинаково.

    Идея контрактного теста:

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

    Качество кода: стиль, статический анализ, архитектурные границы

    Качество кода в PHP обычно держится на трёх слоях:

  • единый стиль (чтобы код читался одинаково)
  • статический анализ (чтобы находить ошибки до запуска)
  • правила архитектуры (чтобы слои не смешивались)
  • Стиль кода и форматирование

    Рекомендуемый базовый стандарт для проектов — PSR-12.

  • PSR-12: Extended Coding Style Guide
  • Инструменты:

  • PHP CS Fixer
  • PHP_CodeSniffer
  • Практический смысл:

  • меньше споров в команде
  • проще ревью
  • меньше «шума» в diff
  • Статический анализ

    Статический анализ проверяет код без запуска и находит:

  • несоответствия типов
  • потенциальные null-ошибки
  • недостижимый код
  • нарушения контрактов
  • Два популярных инструмента:

  • PHPStan
  • Psalm
  • Как связать с темами курса:

  • строгая типизация и явные возвращаемые типы существенно повышают пользу статанализа
  • когда вы оборачиваете исключения и используете свои типы, система становится более проверяемой
  • Composer scripts как минимальная автоматизация качества

    Чтобы качество было привычкой, проверки должны запускаться одной командой.

    Пример идеи для composer.json:

    Тогда:

  • composer test
  • composer stan
  • composer cs
  • CI: почему проверки должны быть в конвейере

    CI (continuous integration) — это запуск тестов и проверок на сервере при каждом пуше/PR.

    Польза:

  • одинаковая проверка для всех
  • меньше «у меня локально работает»
  • регрессии ловятся до мержа
  • Если вы используете GitHub, практический вариант — GitHub Actions.

  • GitHub Actions Documentation
  • Оптимизация производительности: сначала измеряем, потом улучшаем

    Ключевой принцип

    Оптимизация без измерений часто приводит к:

  • усложнению кода
  • микровыигрышам там, где узкое место вообще в другом
  • Правильный процесс:

  • определить метрику (время ответа, количество запросов к БД, память)
  • измерить текущую точку
  • найти узкое место
  • улучшить
  • снова измерить
  • !Цикл оптимизации показывает, что улучшения делаются итеративно и на основе измерений

    Где обычно реальные узкие места в PHP веб-приложениях

    Чаще всего тормозит не «скорость языка», а I/O:

  • лишние запросы к БД, отсутствие индексов, N+1
  • ожидание внешних API
  • чтение/запись файлов
  • отсутствие кэширования
  • Связь с темой PDO:

  • одна оптимизация уровня архитектуры (например, убрать N+1 и сделать один запрос) может дать больше, чем любые микротрюки на уровне PHP
  • Профилирование в PHP

    Профилирование — это сбор данных о том, какие функции и участки кода занимают время.

    Один из самых известных инструментов в PHP — Xdebug profiler.

  • Xdebug Profiler
  • Практическая идея:

  • включить профилирование локально
  • получить профиль
  • найти, где реально тратится время
  • OPcache и настройка исполнения

    OPcache кэширует байткод PHP и значительно ускоряет типичный продакшен.

  • OPcache
  • Практический минимум:

  • убедиться, что OPcache включён в продакшене
  • не хранить большие объёмы логики в динамически генерируемых файлах, которые постоянно инвалидируют кэш
  • Микрооптимизации: что обычно не стоит делать первым

    Примеры типичных преждевременных оптимизаций:

  • переписывать понятный код ради «меньше аллокаций» без профиля
  • заменять обычные циклы на хитрые конструкции без выигрыша в узком месте
  • Когда микрооптимизации оправданы:

  • вы профилировали и видите горячую точку
  • это действительно вычислительный участок, а не I/O
  • Производительность БД в контексте PHP

    Даже идеальный PHP-код будет медленным, если база работает плохо.

    Минимальные практики

  • используйте индексы под реальные условия WHERE и JOIN
  • избегайте N+1 (цикл, который внутри делает отдельный запрос на каждый элемент)
  • выбирайте только нужные колонки, а не SELECT *
  • используйте транзакции для согласованности и часто для снижения накладных расходов
  • Наблюдаемость: логируйте то, что помогает ускорять

    Для оптимизации важны данные. Полезные вещи, которые можно логировать:

  • время выполнения ключевых операций (например, обработчика HTTP)
  • количество запросов к БД на запрос
  • сообщения исключений инфраструктуры (с сохранением причины)
  • При этом сохраняйте правило из темы безопасности:

  • в логах не должно быть секретов и персональных данных в открытом виде
  • Итоги

    В этой статье вы собрали «инженерный контур» вокруг PHP 8.0 приложения:

  • разобрали пирамиду тестирования и роли unit, интеграционных и E2E тестов
  • увидели, как строгая типизация, исключения и SOLID делают код тестируемым
  • освоили базовую модель PHPUnit-тестов, тест-дублей и проверки исключений
  • узнали, как повышать качество кода через стиль, статический анализ и CI
  • закрепили правильный подход к производительности: измерения, профилирование, устранение реальных узких мест