Easy📖Теория8 min

Веб-разработка и паттерны

Сессии, HTTP, Dependency Injection, сложность алгоритмов, обработка ошибок, PHP-FPM

Веб-разработка и паттерны

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

Сессии в PHP

Сессии позволяют сохранять состояние между HTTP-запросами. PHP хранит данные на сервере, а клиенту передаёт только идентификатор сессии (обычно через cookie PHPSESSID).

Базовая работа с сессиями

<?php
declare(strict_types=1);

// Start session — MUST be called before any output
session_start();

// Write data to session
$_SESSION['user_id'] = 42;
$_SESSION['role'] = 'admin';
$_SESSION['login_time'] = time();

// Read data
$userId = $_SESSION['user_id'] ?? null;

// Check if key exists
if (isset($_SESSION['role'])) {
    echo "Role: {$_SESSION['role']}";
}

// Remove specific key
unset($_SESSION['role']);

// Destroy entire session (logout)
$_SESSION = [];                    // Clear data
if (ini_get('session.use_cookies')) {
    $params = session_get_cookie_params();
    setcookie(
        session_name(),
        '',
        time() - 42000,
        $params['path'],
        $params['domain'],
        $params['secure'],
        $params['httponly'],
    );
}
session_destroy();                 // Destroy session file

Безопасность сессий

<?php
declare(strict_types=1);

// Regenerate session ID to prevent session fixation
session_start();
session_regenerate_id(true); // true = delete old session file

// Secure session configuration (php.ini or runtime)
ini_set('session.cookie_httponly', '1');  // No JavaScript access
ini_set('session.cookie_secure', '1');   // HTTPS only
ini_set('session.cookie_samesite', 'Strict'); // CSRF protection
ini_set('session.use_strict_mode', '1'); // Reject uninitialized session IDs
ini_set('session.use_only_cookies', '1'); // No session ID in URLs
ini_set('session.gc_maxlifetime', '1800'); // 30 minutes

Пользовательский обработчик сессий

PHP позволяет заменить стандартное файловое хранилище сессий на собственную реализацию (Redis, Memcached, база данных).

<?php
declare(strict_types=1);

final class RedisSessionHandler extends \SessionHandler
{
    private \Redis $redis;

    public function __construct(
        private readonly string $host = '127.0.0.1',
        private readonly int $port = 6379,
        private readonly int $ttl = 1800,
    ) {
        $this->redis = new \Redis();
    }

    public function open(string $path, string $name): bool
    {
        return $this->redis->connect($this->host, $this->port);
    }

    public function close(): bool
    {
        return $this->redis->close();
    }

    public function read(string $id): string|false
    {
        $data = $this->redis->get("session:{$id}");
        return $data !== false ? $data : '';
    }

    public function write(string $id, string $data): bool
    {
        return $this->redis->setex("session:{$id}", $this->ttl, $data);
    }

    public function destroy(string $id): bool
    {
        return $this->redis->del("session:{$id}") > 0;
    }

    public function gc(int $max_lifetime): int|false
    {
        // Redis handles expiration via TTL — nothing to do
        return 0;
    }
}

// Register custom handler
$handler = new RedisSessionHandler('127.0.0.1', 6379, 3600);
session_set_save_handler($handler, true);
session_start();

Варианты хранения сессий

Хранилище Плюсы Минусы
files (по умолчанию) Простота, не нужны зависимости Медленно при I/O, не масштабируется
redis Быстро, TTL из коробки Дополнительный сервис
memcached Очень быстро Данные теряются при перезапуске
database Надёжность, аудит Нагрузка на БД

Для собеседования: session_start() должен вызываться ДО любого вывода (до HTML, echo, пробелов перед <?php). session_regenerate_id(true) необходимо вызывать при смене привилегий (логин) для защиты от session fixation. Стандартный обработчик --- файлы в директории session.save_path.

HTTP-методы и идемпотентность

Методы HTTP

<?php
declare(strict_types=1);

// Determining HTTP method in PHP
$method = $_SERVER['REQUEST_METHOD']; // GET, POST, PUT, DELETE, PATCH, etc.

// Modern approach — via PSR-7 (example with framework)
// $request->getMethod(); // Returns string like 'GET'

Таблица HTTP-методов

Метод Назначение Идемпотентный Безопасный Тело запроса
GET Получить ресурс Да Да Нет
HEAD Получить заголовки Да Да Нет
POST Создать ресурс Нет Нет Да
PUT Полностью заменить ресурс Да Нет Да
PATCH Частично обновить ресурс Нет Нет Да
DELETE Удалить ресурс Да Нет Нет (обычно)
OPTIONS Запросить доступные методы Да Да Нет

Идемпотентность

<?php
declare(strict_types=1);

// IDEMPOTENT: repeating the request has the same effect
// GET /users/42 — always returns same user (no side effects)
// PUT /users/42 {"name": "Alice"} — always sets name to "Alice"
// DELETE /users/42 — first call deletes, subsequent calls get 404 (same end state)

// NOT IDEMPOTENT:
// POST /orders {"item": "book"} — each call creates a NEW order

// Example: idempotent API endpoint
final class UserController
{
    // PUT is idempotent — same request always produces same result
    public function update(int $id, array $data): array
    {
        // Replace entire resource
        $user = $this->repository->find($id)
            ?? throw new \RuntimeException('User not found');

        $user->name = $data['name'];
        $user->email = $data['email'];
        $this->repository->save($user);

        return ['id' => $user->id, 'name' => $user->name];
    }

    // POST is NOT idempotent — each call creates new resource
    public function create(array $data): array
    {
        $user = new User($data['name'], $data['email']);
        $this->repository->save($user);

        return ['id' => $user->id]; // New ID each time
    }
}

Определение для собеседования: Идемпотентный метод --- метод, повторный вызов которого с теми же параметрами приводит к тому же результату на сервере. GET, PUT, DELETE, HEAD, OPTIONS --- идемпотентны. POST, PATCH --- НЕ идемпотентны. Безопасный метод --- не изменяет состояние сервера (GET, HEAD, OPTIONS).

Dependency Injection (DI)

Dependency Injection --- паттерн, при котором зависимости передаются объекту извне, а не создаются внутри.

Три типа внедрения

<?php
declare(strict_types=1);

// Interface for dependency
interface LoggerInterface
{
    public function log(string $message): void;
}

final class FileLogger implements LoggerInterface
{
    public function log(string $message): void
    {
        file_put_contents('/tmp/app.log', $message . "\n", FILE_APPEND);
    }
}

// 1. Constructor Injection — PREFERRED
final class OrderService
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly OrderRepositoryInterface $repository,
    ) {}

    public function createOrder(array $data): Order
    {
        $order = new Order($data);
        $this->repository->save($order);
        $this->logger->log("Order {$order->id} created");
        return $order;
    }
}

// 2. Setter Injection — for optional dependencies
final class ReportGenerator
{
    private ?LoggerInterface $logger = null;

    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function generate(): string
    {
        $this->logger?->log('Generating report');
        return 'Report data';
    }
}

// 3. Interface Injection — via contract
interface LoggerAwareInterface
{
    public function setLogger(LoggerInterface $logger): void;
}

final class NotificationService implements LoggerAwareInterface
{
    private LoggerInterface $logger;

    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
}

IoC vs DI vs Service Locator

<?php
declare(strict_types=1);

// SERVICE LOCATOR (anti-pattern!) — class asks container for dependencies
final class BadService
{
    public function doWork(): void
    {
        // Bad: hidden dependency, hard to test
        $logger = Container::get(LoggerInterface::class);
        $logger->log('working');
    }
}

// DEPENDENCY INJECTION — dependencies declared explicitly
final class GoodService
{
    // Good: dependency is visible, testable, replaceable
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function doWork(): void
    {
        $this->logger->log('working');
    }
}

// IoC (Inversion of Control) — abstract principle
// Instead of: "class creates its dependencies"
// We use: "dependencies are provided from outside"
// DI is a specific IMPLEMENTATION of IoC

PSR-11: ContainerInterface

<?php
declare(strict_types=1);

// PSR-11 defines a standard interface for DI containers
namespace Psr\Container;

interface ContainerInterface
{
    // Returns entry by its identifier
    public function get(string $id): mixed;

    // Returns true if entry exists
    public function has(string $id): bool;
}

// Usage in framework bootstrap (NOT in business logic!)
final class Kernel
{
    public function __construct(
        private readonly ContainerInterface $container,
    ) {}

    public function handleRequest(string $controllerClass): mixed
    {
        if (!$this->container->has($controllerClass)) {
            throw new \RuntimeException("Service not found: {$controllerClass}");
        }

        $controller = $this->container->get($controllerClass);
        return $controller->handle();
    }
}

Для собеседования: DI --- конкретная реализация принципа IoC (Inversion of Control). Service Locator --- антипаттерн, потому что скрывает зависимости (вызывающий код не знает, что нужно классу). Constructor injection --- предпочтительный способ, потому что зависимости явные и обязательные. PSR-11 определяет get() и has() для контейнеров.

Сложность алгоритмов (Big O)

На собеседованиях часто проверяют понимание алгоритмической сложности. Это важно для выбора оптимальных структур данных и алгоритмов.

Основные сложности

<?php
declare(strict_types=1);

// O(1) — Constant time: result independent of input size
function getFirst(array $items): mixed
{
    return $items[0] ?? null; // Always one operation
}
// Array access by key, hash table lookup, stack push/pop

// O(log n) — Logarithmic: halves search space each step
function binarySearch(array $sorted, int $target): int
{
    $low = 0;
    $high = count($sorted) - 1;

    while ($low <= $high) {
        $mid = intdiv($low + $high, 2);
        if ($sorted[$mid] === $target) return $mid;
        if ($sorted[$mid] < $target) $low = $mid + 1;
        else $high = $mid - 1;
    }
    return -1;
}

// O(n) — Linear: proportional to input size
function linearSearch(array $items, mixed $target): int
{
    foreach ($items as $index => $item) {
        if ($item === $target) return $index;
    }
    return -1;
}
// in_array(), array_search(), foreach

// O(n log n) — Linearithmic: efficient sorting
// sort(), usort(), array_multisort() — use introsort internally

// O(n²) — Quadratic: nested loops
function bubbleSort(array &$items): void
{
    $n = count($items);
    for ($i = 0; $i < $n; $i++) {
        for ($j = 0; $j < $n - $i - 1; $j++) {
            if ($items[$j] > $items[$j + 1]) {
                [$items[$j], $items[$j + 1]] = [$items[$j + 1], $items[$j]];
            }
        }
    }
}

// O(2^n) — Exponential: doubles with each input unit
function fibonacci(int $n): int
{
    if ($n <= 1) return $n;
    return fibonacci($n - 1) + fibonacci($n - 2); // Two recursive calls
}

Сложность операций PHP-массивов

Операция Сложность Примечание
$arr[$key] O(1) Hash table lookup
isset($arr[$key]) O(1) Hash table check
$arr[] = $val O(1) amortized Append
in_array($val, $arr) O(n) Linear scan
array_search() O(n) Linear scan
array_key_exists() O(1) Hash table check
sort() O(n log n) Introsort
array_unique() O(n log n) Sort-based
array_merge() O(n + m) Linear
array_diff() O(n * m) Worst case

Совет для собеседования: isset() и array_key_exists() работают за O(1) --- используйте их вместо in_array() (O(n)), когда ищете по ключу. Если нужно часто проверять наличие значения --- переверните массив через array_flip() и ищите по ключу.

Обработка ошибок

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

<?php
declare(strict_types=1);

// ERRORS — legacy mechanism, can be converted to exceptions
// E_ERROR, E_WARNING, E_NOTICE, E_DEPRECATED, etc.

// Custom error handler — convert errors to exceptions
set_error_handler(function (
    int $severity,
    string $message,
    string $file,
    int $line,
): bool {
    // Don't handle suppressed errors (@operator)
    if (!(error_reporting() & $severity)) {
        return false;
    }

    throw new \ErrorException($message, 0, $severity, $file, $line);
});

// EXCEPTIONS — modern approach
try {
    $result = riskyOperation();
} catch (\InvalidArgumentException $e) {
    // Handle specific exception
    echo "Invalid input: {$e->getMessage()}";
} catch (\RuntimeException | \LogicException $e) {
    // Catch multiple types (PHP 8.0+)
    echo "Error: {$e->getMessage()}";
} catch (\Throwable $e) {
    // Catch any error or exception
    echo "Unexpected: {$e->getMessage()}";
} finally {
    // ALWAYS executed — whether exception occurred or not
    cleanup();
}

Пользовательские исключения

<?php
declare(strict_types=1);

// Base domain exception
abstract class DomainException extends \RuntimeException
{
    public function __construct(
        string $message,
        public readonly array $context = [],
        int $code = 0,
        ?\Throwable $previous = null,
    ) {
        parent::__construct($message, $code, $previous);
    }
}

// Specific domain exceptions
final class OrderNotFoundException extends DomainException
{
    public static function byId(int $id): self
    {
        return new self(
            message: "Order #{$id} not found",
            context: ['order_id' => $id],
            code: 404,
        );
    }
}

final class InsufficientBalanceException extends DomainException
{
    public static function forAmount(float $required, float $available): self
    {
        return new self(
            message: "Insufficient balance: need {$required}, have {$available}",
            context: [
                'required' => $required,
                'available' => $available,
            ],
            code: 422,
        );
    }
}

// Usage with named constructors
throw OrderNotFoundException::byId(42);
throw InsufficientBalanceException::forAmount(100.0, 50.0);

Иерархия Throwable в PHP

Throwable (interface)
├── Error (internal PHP errors)
│   ├── TypeError
│   ├── ValueError
│   ├── ArithmeticError
│   │   └── DivisionByZeroError
│   ├── FiberError
│   └── UnhandledMatchError
└── Exception
    ├── LogicException
    │   ├── BadFunctionCallException
    │   ├── BadMethodCallException
    │   ├── DomainException
    │   ├── InvalidArgumentException
    │   ├── LengthException
    │   └── OutOfRangeException
    └── RuntimeException
        ├── OutOfBoundsException
        ├── OverflowException
        ├── RangeException
        ├── UnderflowException
        └── UnexpectedValueException

Для собеседования: Error --- системные ошибки PHP (TypeError, ValueError). Exception --- ошибки приложения. Оба реализуют Throwable. catch (\Throwable $e) перехватывает ВСЁ. finally выполняется всегда, даже если в catch есть return. Используйте SPL-исключения для стандартных ситуаций.

PHP-FPM: архитектура и жизненный цикл

CGI vs FastCGI vs PHP-FPM

CGI (устаревший):
  Запрос → Веб-сервер → Создать процесс PHP → Выполнить → Убить процесс
  Проблема: новый процесс на КАЖДЫЙ запрос

FastCGI:
  Запрос → Веб-сервер → Переиспользовать процесс PHP → Выполнить → Вернуть в пул
  Улучшение: процессы живут долго и обрабатывают много запросов

PHP-FPM (FastCGI Process Manager):
  Продвинутая реализация FastCGI с управлением пулами процессов

Жизненный цикл запроса в PHP-FPM

1. Master process запускается и создаёт пул worker-процессов
2. Nginx получает HTTP-запрос
3. Nginx передаёт запрос PHP-FPM через FastCGI (unix socket или TCP)
4. PHP-FPM выбирает свободный worker из пула
5. Worker выполняет PHP-скрипт:
   a. Инициализация (загрузка файлов, autoload)
   b. Выполнение кода
   c. Формирование ответа
6. Worker отправляет ответ обратно Nginx
7. Worker возвращается в пул (память НЕ освобождается полностью!)
8. Nginx отправляет ответ клиенту

Менеджеры процессов

; /etc/php/8.4/fpm/pool.d/www.conf

; static — fixed number of workers (predictable memory usage)
pm = static
pm.max_children = 20

; dynamic — adjusts workers based on load
pm = dynamic
pm.max_children = 50      ; Maximum workers
pm.start_servers = 5      ; Workers at startup
pm.min_spare_servers = 3  ; Minimum idle workers
pm.max_spare_servers = 10 ; Maximum idle workers

; ondemand — creates workers only when needed (saves memory)
pm = ondemand
pm.max_children = 50
pm.process_idle_timeout = 10s ; Kill idle workers after 10s

; Safety settings
pm.max_requests = 500     ; Restart worker after N requests (prevents memory leaks)
request_terminate_timeout = 30s ; Kill worker if request takes too long

Формула расчёта pm.max_children

pm.max_children = Доступная_RAM / Средний_расход_на_процесс

Пример:
  Сервер: 4 GB RAM
  ОС и другие сервисы: 1 GB
  Доступно для PHP-FPM: 3 GB
  Средний worker: 50 MB

  pm.max_children = 3072 MB / 50 MB = ~60

Для собеседования: PHP-FPM --- менеджер процессов FastCGI. Master-процесс управляет пулом worker-процессов. Три режима: static (фиксированное число), dynamic (адаптируется к нагрузке), ondemand (создаёт по запросу). pm.max_requests защищает от утечек памяти, перезапуская worker после N запросов. Каждый запрос --- изолированное выполнение, но worker переиспользуется.


Проверь себя

🧪

Какова сложность операции in_array() в PHP?

🧪

Какой HTTP-метод НЕ является идемпотентным?

🧪

Что делает параметр pm.max_requests в PHP-FPM?

🧪

Почему Service Locator считается антипаттерном?

🧪

Зачем вызывать session_regenerate_id(true) при логине пользователя?