Веб-разработка и паттерны
Раздел охватывает практические вопросы о веб-разработке на 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 переиспользуется.