Skip to content

PHP Шпаргалка

Популярный скриптовый язык общего назначения для веб-разработки.

01

Основы

Переменные, типы и константы

Переменные PHP начинаются с $ и являются динамически типизированными. PHP 7.4+ поддерживает типизированные свойства. Используйте define() для констант времени выполнения и const для констант времени компиляции (быстрее). PHP 8.1 представил enum — типизированную систему перечислений, превосходящую константы классов. Всегда объявляйте типы, где возможно (PHP 7+), для раннего обнаружения ошибок и лучшей поддержки IDE.

php
<?php
$name = "Alice";          // string
$age = 30;                // integer
$price = 19.99;           // float (double)
$active = true;           // boolean
$items = [1, 2, 3];       // array
$null = null;             // null

// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159;       // compile-time constant
echo MAX_USERS;            // no $ prefix

// PHP 8.1+ enums
enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
}
$s = Status::Active;

Echo, print и отладка

echo — языковая конструкция (не функция) — самая быстрая для вывода. print возвращает 1, поэтому может использоваться в выражениях. printf/sprintf используют C-стиль спецификаторов формата (%s строка, %d int, %f float, %x hex). var_dump() — основной инструмент отладки — показывает типы и значения. error_log() пишет в лог ошибок PHP или syslog. В продакшене никогда не показывайте отладочный вывод пользователям.

php
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";

// print: returns 1, single arg
print "Hello World";

// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);

// debugging output
print_r($array);           // human-readable
var_dump($variable);       // type + value (detailed)
var_export($array, true);  // valid PHP code (for caching)

// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log);           // log to error log

Операторы и сравнения

Всегда используйте === (строгое сравнение) для избежания багов приведения типов. == преобразует типы перед сравнением, что даёт неожиданные результаты (0 == 'abc' было true в PHP 7). Оператор корабля (<=>) возвращает -1/0/1 — полезен для usort. Оператор объединения с null (??) — идиоматичный способ предоставить значения по умолчанию. Null-safe оператор (?->) (PHP 8+) сокращённо обрабатывает цепочки методов на null, заменяя многословные проверки isset().

php
<?php
// arithmetic
$sum = 10 + 3;       // 13
$mod = 10 % 3;       // 1
$pow = 2 ** 3;       // 8 (PHP 5.6+)

// comparison: == vs ===
echo (0 == "abc");   // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc");  // false (strict — type + value)
echo ("1" == 1);     // true (loose)
echo ("1" === 1);    // false (strict)

// spaceship operator (PHP 7+)
echo 1 <=> 2;        // -1 (less than)
echo 2 <=> 2;        // 0 (equal)
echo 3 <=> 2;        // 1 (greater than)

// null coalescing
$name = $input ?? "default";    // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";

// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is null

Суперглобалы и веб

Суперглобалы — встроенные ассоциативные массивы, доступные во всех областях видимости. $_GET и $_POST содержат пользовательский ввод — ВСЕГДА санитизируйте/валидируйте перед использованием. filter_input() безопаснее прямого доступа. Никогда не доверяйте значениям $_SERVER, которые могут быть подделаны клиентами (например, HTTP_USER_AGENT). Всегда вызывайте exit после header('Location:') — иначе PHP продолжит выполнение. Начинайте сессии с session_start() до любого вывода.

php
<?php
// superglobals available everywhere
$_GET['name'];           // query string params
$_POST['email'];         // form POST data
$_REQUEST['x'];          // GET + POST + COOKIE
$_SERVER['HTTP_HOST'];   // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session'];     // cookies
$_FILES['upload'];       // file uploads
$_SESSION['user_id'];    // session data (after session_start())

// get client IP
$ip = $_SERVER['REMOTE_ADDR'];

// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}

// redirect
header("Location: /dashboard");
exit; // always exit after redirect

Include и require

include/require выполняют указанный файл. require вызывает фатальную ошибку при отсутствии файла (используйте для критичных зависимостей); include только предупреждает (используйте для опциональных шаблонов). Варианты _once отслеживают включённые файлы для предотвращения двойного включения — необходимы для определений функций/классов. Автозагрузчик Composer (require_once 'vendor/autoload.php') устраняет ручное управление include. Файлы могут возвращать значения, что полезно для конфигурации.

php
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once

// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload

// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)

// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];
02

Строки

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

В PHP более 100 строковых функций. strpos() возвращает false, если не найдено — используйте === false для проверки (0 — валидная позиция). str_replace() может принимать массивы для поиска/замены. substr() поддерживает отрицательные смещения (с конца). Для мультибайтных строк (UTF-8) используйте mb_* эквиваленты (mb_strlen, mb_substr) — strlen считает байты, а не символы. Всегда устанавливайте default_charset='UTF-8' и используйте mb_* функции для не-ASCII текста.

php
<?php
$s = "Hello, World";

// length and case
echo strlen($s);              // 12
echo str_word_count($s);      // 2
echo strtoupper($s);          // HELLO, WORLD
echo strtolower($s);          // hello, world
echo ucfirst("hello");        // Hello
echo ucwords("hello world");  // Hello World

// search and replace
echo strpos($s, "World");     // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5);        // Hello
echo substr($s, -5);          // World
echo strrev($s);              // dlroW ,olleH

// trim
echo trim("  hi  ");          // "hi"
echo ltrim("  hi");           // "hi"
echo rtrim("hi  ");           // "hi"

Интерполяция строк и heredoc

Строки в двойных кавычках интерполируют переменные; в одинарных — нет (используйте одинарные для литерального текста — немного быстрее). Используйте {$var} для сложных выражений (свойства объекта, доступ к массиву, вызовы методов). Heredoc (<<<ID) идеален для многострочных строк вроде SQL или HTML — интерполирует переменные. Nowdoc (<<<'ID') — неинтерпретируемая версия, полезная для regex-шаблонов с обратными слэшами.

php
<?php
$name = "Alice";
$age = 30;

// double quotes: variable interpolation
echo "Hello, $name!";          // Hello, Alice!
echo "Hello, {$name}!";        // Hello, Alice! (braces for clarity)
echo "Age: {$age}";            // Age: 30

// single quotes: no interpolation (faster)
echo 'Hello, $name';           // Hello, $name (literal)

// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";

// heredoc: multi-line, interpolates
$sql = <<<SQL
    SELECT * FROM users
    WHERE name = '$name'
    AND age > $age
SQL;

// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
    \d{3}-\d{4}
REGEX;

sprintf и форматирование

sprintf() необходима для безопасного построения форматированных строк — в отличие от интерполяции строк, она обрабатывает преобразования типов и заполнение. Используйте %d для целых чисел (не %s) для числового форматирования. number_format() форматирует числа с разделителями разрядов — критично для отображения валюты. Всегда используйте sprintf для SQL-фрагментов в подготовленных запросах (хотя подготовленные запросы всё равно требуются для пользовательского ввода).

php
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
    "%-10s | %5d | %8.2f",
    "Alice", 42, 19.99
);
echo $formatted;  // "Alice      |    42 |    19.99"

// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d  right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals

// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice

// number_format
echo number_format(1234567.891, 2);  // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European format

Regex (PCRE)

PHP использует PCRE (Perl-Compatible Regular Expressions) с разделителями /pattern/. preg_match возвращает 1 при совпадении, 0 при отсутствии (используйте ===, не ==, так как 0 falsy). Всегда валидируйте пользовательский ввод regex, но не полагайтесь только на него — используйте filter_var() для email, URL. preg_replace мощный, но может быть медленным на больших строках. Используйте [^...] для белого списка символов, а не чёрного списка. Флаг i делает сопоставление нечувствительным к регистру.

php
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
    echo "alphabetic";
}

// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"

// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]

// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld

// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three  four");
// ["one", "two", "three", "four"]

Мультибайтность и кодировки

Строковые функции PHP по умолчанию байт-ориентированные, а не символьно-ориентированные — они ломаются на мультибайтных символах (UTF-8, китайский, эмодзи). Всегда используйте mb_* функции (mb_strlen, mb_substr, mb_strpos, mb_strtoupper) для не-ASCII текста. Устанавливайте mb_internal_encoding('UTF-8') в начале приложения. Используйте JSON_UNESCAPED_UNICODE для сохранения китайского/эмодзи читаемым в JSON-выводе. Это частый источник багов в международных приложениях.

php
<?php
// UTF-8 strings: use mb_* functions
$text = "Café";  // 4 chars, 5 bytes (é = 2 bytes)

echo strlen($text);      // 5 (bytes!)
echo mb_strlen($text);   // 4 (characters!)

echo substr($text, 0, 3);    // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)

// case conversion
echo strtoupper("straße");    // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)

// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');

// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');

// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
03

Массивы

Индексные и ассоциативные массивы

Массивы PHP — фактически упорядоченные hash maps — они работают и как списки, и как словари. Индексные массивы автоматически назначают числовые ключи; ассоциативные используют строковые ключи. isset() возвращает false для null-значений; array_key_exists() возвращает true даже для null. unset() удаляет элемент, но не переиндексирует. Для истинного списка (без пробелов) используйте array_values() для переиндексации после удаления. В PHP 8.1+ есть тип массива readonly.

php
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40;              // append
echo $nums[0];             // 10
echo count($nums);         // 4

// associative array (string keys)
$user = [
    "name" => "Alice",
    "age" => 30,
    "email" => "[email protected]",
];
echo $user["name"];        // Alice
$user["phone"] = "555-1234"; // add key

// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];

// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }

// remove element
unset($user["phone"]);

Многомерность и итерация

Многомерные массивы — массивы массивов. foreach — идиоматичный способ итерации — быстрее и читаемее циклов for. Используйте &$value для изменения элементов на месте (всегда снимайте ссылку после цикла во избежание багов). array_column() извлекает один столбец из 2D-массива — крайне полезно для преобразования наборов результатов БД. Массивы PHP сохраняют порядок вставки.

php
<?php
$users = [
    ["name" => "Alice", "age" => 30],
    ["name" => "Bob", "age" => 25],
    ["name" => "Carol", "age" => 35],
];

// iterate with key + value
foreach ($users as $index => $user) {
    echo "$index: {$user['name']} ({$user['age']})\n";
}

// modify by reference
foreach ($users as &$user) {
    $user['age'] += 1; // increment each age
}
unset($user); // break reference!

// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
    foreach ($row as $cell) {
        echo $cell . " ";
    }
    echo "\n";
}

// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]

Функции массивов: map, filter, reduce

array_map, array_filter и array_reduce — функциональное трио для массивов. Стрелочные функции (fn() =>) делают их лаконичными. array_filter сохраняет ключи — используйте array_values() для переиндексации при необходимости. array_merge переиндексирует числовые ключи, но сохраняет строковые (поздние значения перезаписывают). array_column, array_chunk и array_slice необходимы для манипуляции данными. Эти функции — основа обработки данных в PHP.

php
<?php
$nums = [1, 2, 3, 4, 5];

// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums);  // [2, 4, 6, 8, 10]

// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0);  // [2, 4]

// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0);  // 15

// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);

// combining arrays
$merged = array_merge([1, 2], [3, 4]);        // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2);            // [2, 3]
$chunked = array_chunk($nums, 2);              // [[1,2], [3,4], [5]]

Сортировка массивов

Функции сортировки PHP модифицируют массив на месте (передача по ссылке). sort/rsort переиндексируют; asort/arsort сохраняют ключи. usort с функцией сравнения (используя <=>) сортирует по кастомной логике. natsort() выполняет естественную сортировку (img2 перед img10) — необходимо для имён файлов. Для многомерных массивов используйте usort с замыканием, сравнивающим нужное поле. Оператор корабля (<=>) упрощает функции сравнения.

php
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];

// sort by value (reindexes)
sort($nums);                          // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums);                         // descending

// sort preserving keys
asort($nums);   // ascending, preserve keys
arsort($nums);  // descending, preserve keys

// sort by key
ksort($nums);   // by key ascending
krsort($nums);  // by key descending

// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending

// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files);    // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)

Инспекция и манипуляция массивами

in_array с strict=true (третий параметр) предотвращает баги приведения типов. array_search возвращает ключ (используйте === false для проверки). array_push/pop реализуют LIFO (стек); array_shift/unshift реализуют FIFO (очередь) — но shift имеет сложность O(n). Для больших очередей используйте SplQueue или SplDoublyLinkedList. array_unique сохраняет ключи. array_diff/intersect сравнивают значения; используйте array_diff_key/intersect_key для сравнения по ключам.

php
<?php
$arr = [1, 2, 3, 4, 5];

// inspection
echo count($arr);                  // 5
echo in_array(3, $arr);            // true
echo in_array("3", $arr, true);    // false (strict)
echo array_search(3, $arr);        // 2 (key, false if not found)
print_r(array_keys($arr));         // [0, 1, 2, 3, 4]
print_r(array_values($arr));       // [1, 2, 3, 4, 5]

// stack/queue operations
array_push($arr, 6);    // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front

// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]

// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]
04

Поток управления

If / Else / Elseif

PHP использует elseif (одно слово) — не 'else if' с пробелом (хотя это тоже работает). Альтернативный синтаксис (if: ... endif;) полезен в HTML-шаблонах для избежания путаницы со скобками. Тернарный оператор правоассоциативен — избегайте вложенности. Оператор присваивания с объединением null (??=) устанавливает значение, только если оно сейчас null — идеально для ленивой инициализации значений конфигурации по умолчанию.

php
<?php
$score = 85;

if ($score >= 90) {
    $grade = "A";
} elseif ($score >= 80) {
    $grade = "B";
} elseif ($score >= 70) {
    $grade = "C";
} else {
    $grade = "F";
}

// alternative syntax (for templates)
if ($score >= 90):
    echo "Excellent";
elseif ($score >= 80):
    echo "Good";
else:
    echo "Try harder";
endif;

// ternary
$status = $age >= 18 ? "adult" : "minor";

// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not set

Switch и match

switch использует нестрогое сравнение (==) и требует break для предотвращения провалов — частый источник багов. match (PHP 8+) использует строгое сравнение (===), возвращает значение напрямую и выбрасывает исключение, если ни одна ветвь не совпала (без тихих неудач). match — современная замена switch, когда нужно значение. Используйте switch для сложных многострочных случаев; match — для простого выбора значения. Всегда включайте ветвь default.

php
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
    case "Mon":
    case "Tue":
    case "Wed":
        echo "Weekday";
        break;
    case "Sat":
    case "Sun":
        echo "Weekend";
        break;
    default:
        echo "Unknown";
}

// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
    200, 201 => "Success",
    301, 302 => "Redirect",
    404 => "Not Found",
    500 => "Server Error",
    default => "Unknown",
};
// match throws UnhandledMatchError if no match and no default

Циклы: for, while, foreach, do-while

foreach — идиоматичный цикл для массивов — быстрее и безопаснее for с count(). Используйте continue для пропуска итераций и break для выхода. В PHP нет помеченных break/continue (в отличие от Java/Rust). Для ассоциативных массивов foreach ($arr as $key => $value) — стандартный шаблон. do-while выполняется хотя бы один раз — полезно для валидации ввода. Избегайте модификации массива во время foreach (используйте отдельный массив для результатов).

php
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
    echo $i;  // 01234
}

// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
    echo $fruit;
}

// foreach with key
foreach ($fruits as $index => $fruit) {
    echo "$index: $fruit";
}

// while
$count = 0;
while ($count < 3) {
    echo $count++;
}

// do-while (runs at least once)
do {
    $line = readline("> ");
} while ($line !== "quit");

// break and continue
for ($i = 0; $i < 10; $i++) {
    if ($i === 3) continue; // skip 3
    if ($i === 7) break;    // stop at 7
    echo $i;
}

Поток управления в шаблонах

Альтернативный синтаксис управления PHP (if:/elseif:/else:/endif;, foreach:/endforeach;) создан для HTML-шаблонов. <?= $var ?> — сокращение для <?php echo $var; ?> — всегда используйте в шаблонах для читаемости. Всегда экранируйте вывод htmlspecialchars() для предотвращения XSS. Разделение PHP-логики и HTML-представления — основа шаблонных систем вроде Twig и Blade, предлагающих более чистый синтаксис и автоматическое экранирование.

php
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
    <div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
    <div class="editor-tools">Edit Tools</div>
<?php else: ?>
    <div class="user-view">Read Only</div>
<?php endif; ?>

<?php foreach ($products as $p): ?>
    <div class="product">
        <?= htmlspecialchars($p['name']) ?>
        - $<?= number_format($p['price'], 2) ?>
    </div>
<?php endforeach; ?>

<?php // shorthand echo ?>
<h1><?= $title ?></h1>

<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>

Исключения и обработка ошибок

PHP 7+ использует исключения для большинства ошибок. Всегда ловите конкретные типы исключений (не только Exception) для корректной обработки различных сбоев. finally выполняется всегда — используйте для очистки (закрытие файлов, соединений). Пользовательские исключения расширяют Exception и добавляют доменный контекст. PHP 8+ позволяет ловить несколько типов исключений через |. Установите PDO в режим исключений для согласованной обработки ошибок. Никогда не ловите исключения без логирования — тихие неудачи скрывают баги.

php
<?php
// try / catch / finally
try {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    error_log("DB connection failed: " . $e->getMessage());
    die("Service unavailable");
} finally {
    // always runs, even after return/throw
    echo "Cleanup";
}

// custom exception
class ValidationException extends Exception {
    public function __construct(string $field, string $message = "") {
        parent::__construct("$field: $message");
    }
}

// throw
if (empty($email)) {
    throw new ValidationException("email", "is required");
}

// catch multiple types (PHP 8+)
try {
    riskyOperation();
} catch (PDOException | RuntimeException $e) {
    // catches either type
    log_error($e);
}
05

Функции

Определение функций

PHP 7+ поддерживает типизированные параметры и возвращаемые типы (int, string, array, ?Type для nullable). PHP 8+ добавляет именованные аргументы (пропуск значений по умолчанию, переупорядочивание параметров), union-типы (int|string) и тип mixed. Вариативные параметры (...$nums) собирают дополнительные аргументы в массив. Оператор spread (...$arr) распаковывает массив как аргументы. Передача по ссылке (&) модифицирует оригинал — используйте умеренно, так как это усложняет понимание кода.

php
<?php
// basic function with return type
function add(int $a, int $b): int {
    return $a + $b;
}

// default parameters
function greet(string $name, string $greeting = "Hello"): string {
    return "$greeting, $name!";
}

// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");

// variadic functions
function sum(int ...$nums): int {
    return array_sum($nums);
}
echo sum(1, 2, 3, 4);  // 10

// spread operator
$nums = [1, 2, 3];
echo sum(...$nums);  // 6

// pass by reference
function increment(int &$n): void {
    $n++;
}
$x = 5;
increment($x);
echo $x;  // 6

Стрелочные функции и замыкания

Стрелочные функции (fn() =>) — лаконичные однострочные замыкания, автоматически захватывающие внешние переменные по значению. Традиционные замыкания (function() use ($var)) нужны для многострочных тел или захвата по ссылке (&$var). Замыкания необходимы для array_map, array_filter, usort и обработчиков событий. Стрелочные функции не могут иметь операторы (без if, for) — используйте традиционные замыкания для сложной логики. Замыкания — объекты первого класса (класс Closure).

php
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5);  // 25

// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5);  // 15

// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
    return $x * $factor + 1;
};

// capture by reference
$count = 0;
$increment = function () use (&$count) {
    $count++;
};
$increment();
echo $count;  // 1

// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);

Область видимости переменных и глобалы

В PHP область видимости на уровне функций — переменные, определённые вне функции, НЕ доступны внутри без 'global' или $GLOBALS. Избегайте 'global' — он создаёт скрытые зависимости и усложняет тестирование. Используйте внедрение зависимостей. static-переменные сохраняются между вызовами функции, но ограничены функцией — полезно для кэширования/мемоизации, но может вызывать проблемы в долго работающих процессах. Замыкания должны явно захватывать переменные через 'use'.

php
<?php
$global = "I'm global";

function testScope(): void {
    // echo $global; // ERROR: not in scope
    global $global;  // import global
    echo $global;    // OK

    // $GLOBALS superglobal (alternative)
    echo $GLOBALS['global'];
}

// static variables: persist across calls
function counter(): int {
    static $count = 0;
    return ++$count;
}
echo counter();  // 1
echo counter();  // 2
echo counter();  // 3

// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
    // echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
    echo $outer;  // OK, captured
};

Объявления типов и строгие типы

declare(strict_types=1) должен быть первым оператором — он включает строгую проверку типов (без приведения) для всего файла. Без него PHP приводит типы (int 5, переданный в строковый параметр, становится '5'). Всегда используйте строгие типы в новом коде. PHP 8+ добавляет union-типы, mixed, never (функция никогда не возвращает) и static (возвращает класс). Синтаксис first-class callable (func(...)) создаёт замыкания из любого callable — чище, чем ссылки на функции.

php
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);

function divide(float $a, float $b): float {
    if ($b === 0.0) {
        throw new DivisionByZeroError();
    }
    return $a / $b;
}

// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
    if ($id === null) return null;
    return "User $id";
}

// union types (PHP 8+)
function process(int|string $input): int|string {
    return is_int($input) ? $input * 2 : strtoupper($input);
}

// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }

// first-class callable syntax (PHP 8.1+)
$func = strlen(...);  // creates Closure from function
echo $func("hello");  // 5

Генераторы и yield

Генераторы (функции с yield) производят значения лениво — они не вычисляют все значения заранее, экономя память. Это необходимо для обработки больших файлов или наборов данных. yield приостанавливает функцию, возвращая значение; функция возобновляется при запросе следующего значения. Генераторы реализуют Iterator, поэтому работают с foreach. Используйте генераторы для: обработки файлов, итерации строк БД, бесконечных последовательностей и конвейеров. Они также могут возвращать пары key=>value и принимать значения через send().

php
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

foreach (readLines("large.txt") as $line) {
    echo $line;  // one line at a time, low memory
}

// infinite generator
function fibonacci(): Generator {
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
    echo $fib->current() . " ";
    $fib->next();
}

// yield with key
function pairs(): Generator {
    yield 'a' => 1;
    yield 'b' => 2;
    yield 'c' => 3;
}
06

ООП и классы

Класс, свойства и конструктор

Продвижение конструктора PHP 8 устраняет шаблонность — объявляйте свойства как параметры конструктора. Видимость свойств: public (везде), protected (класс + подклассы), private (только класс). readonly (PHP 8.1) предотвращает изменение после инициализации. self ссылается на текущий класс; static — на вызывающий класс (для позднего статического связывания). Используйте static:: вместо self:: в иерархиях наследования для корректного полиморфизма.

php
<?php
class Person {
    // typed properties (PHP 7.4+)
    public string $name;
    protected int $age;
    private string $email;
    public readonly string $id;  // PHP 8.1+

    // constructor promotion (PHP 8+)
    public function __construct(
        string $name,
        int $age,
        string $email = "",
        string $id = ""
    ) {
        $this->name = $name;
        $this->age = $age;
        $this->email = $email;
        $this->id = $id;
    }

    // methods
    public function greet(): string {
        return "Hi, I'm {$this->name}";
    }

    // static method
    public static function create(string $name): self {
        return new self($name, 0);
    }
}

$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;

Наследование и абстрактные классы

Абстрактные классы нельзя инстанцировать — они определяют шаблон для подклассов. Абстрактные методы должны быть реализованы конкретными подклассами. PHP поддерживает только одиночное наследование (одно extends). Используйте final для предотвращения наследования/переопределения, когда реализация не должна меняться. protected-члены доступны в подклассах — используйте для внутренних API. Всегда вызывайте parent::__construct(), если у родителя есть конструктор. instanceof проверяет тип: if ($dog instanceof Animal).

php
<?php
abstract class Animal {
    protected string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // abstract method: must be implemented by subclasses
    abstract public function speak(): string;

    // concrete method: inherited as-is
    public function describe(): string {
        return "{$this->name} says {$this->speak()}";
    }
}

class Dog extends Animal {
    public function speak(): string {
        return "Woof";
    }
}

class Cat extends Animal {
    public function speak(): string {
        return "Meow";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof

// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }

Интерфейсы и trait

Интерфейсы определяют контракты — классы могут реализовывать несколько интерфейсов (в отличие от одиночного наследования). Все методы интерфейса должны быть public. Trait обеспечивает переиспользование кода без наследования — это 'copy-paste' на уровне языка. Trait могут иметь свойства, методы и даже абстрактные методы. Используйте trait для сквозной функциональности (временные метки, логирование, soft deletes). Разрешение конфликтов: используйте TraitA::method insteadof TraitB, когда у trait одинаковые имена методов.

php
<?php
// interface: contract (no implementation)
interface Comparable {
    public function compareTo(object $other): int;
}

interface JsonSerializable {
    public function jsonSerialize(): mixed;
}

// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
    public function __construct(public float $price) {}

    public function compareTo(object $other): int {
        return $this->price <=> $other->price;
    }

    public function jsonSerialize(): mixed {
        return ['price' => $this->price];
    }
}

// trait: reusable code (horizontal reuse)
trait Timestampable {
    public DateTime $createdAt;

    public function setCreatedAt(): void {
        $this->createdAt = new DateTime();
    }

    public function age(): DateInterval {
        return $this->createdAt->diff(new DateTime());
    }
}

class Article {
    use Timestampable; // use trait
}

Магические методы

Магические методы — специальные методы, перехватывающие операции с объектами. __get/__set реализуют перегрузку свойств (динамические свойства). __toString включает приведение к строке. __invoke делает объекты вызываемыми. __clone запускается при клонировании (clone $obj). Используйте умеренно — они добавляют 'магию', которую трудно отследить. __get/__set полезны для объектов передачи данных или ленивой загрузки. Всегда чётко документируйте магическое поведение. PHP 8.2 депрекатирует динамические свойства — используйте __get/__set или #[AllowDynamicProperties].

php
<?php
class Magic {
    private array $data = [];

    // called when accessing undefined property
    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }

    // called when setting undefined property
    public function __set(string $name, mixed $value): void {
        $this->data[$name] = $value;
    }

    // called when isset() or empty() on undefined property
    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }

    // called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // called when object is called as function
    public function __invoke(string $arg): string {
        return "Called with: $arg";
    }

    // called on clone
    public function __clone(): void {
        $this->data = []; // reset on clone
    }
}

$m = new Magic();
$m->foo = "bar";       // __set
echo $m->foo;          // __get -> "bar"
echo $m;               // __toString -> {"foo":"bar"}
echo $m("test");       // __invoke

Пространства имён и автозагрузка

Пространства имён предотвращают коллизии имён классов — как пакеты в Java. namespace должен быть первым оператором. use импортирует классы (с опциональными алиасами: use Foo\Bar as B). PSR-4 автозагрузка отображает пространства имён на пути файлов: App\Models\User → src/Models/User.php. Автозагрузчик Composer (require 'vendor/autoload.php') обрабатывает это автоматически. Всегда используйте пространства имён в современном PHP. \\ в строках — экранированный обратный слэш (разделитель пространств имён).

php
<?php
// file: src/Models/User.php
namespace App\Models;

use App\Database\Connection;
use App\Exceptions\UserNotFoundException;

class User {
    private Connection $db;

    public function __construct(Connection $db) {
        $this->db = $db;
    }

    public function find(int $id): ?self {
        // ...
        throw new UserNotFoundException("User $id not found");
    }
}

// composer.json (PSR-4 autoloading)
// {
//   "autoload": {
//     "psr-4": { "App\\": "src/" }
//   }
// }

// usage
use App\Models\User;
$user = new User($db);
07

Веб, формы и файловый ввод-вывод

Обработка форм и валидация

Всегда валидируйте на сервере — клиентская валидация для UX, а не безопасности. filter_input/filter_var с FILTER_VALIDATE_* возвращают false при некорректном вводе. Обрезайте строки перед валидацией. Используйте подготовленные запросы для вставки в БД. CSRF-токены предотвращают подделку межсайтовых запросов — генерируйте для каждой сессии и проверяйте на POST. bin2hex(random_bytes(32)) генерирует криптографически безопасный токен. Никогда не доверяйте пользовательскому вводу — валидируйте, санитизируйте и экранируйте.

php
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
    $age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);

    $errors = [];

    if (empty($name)) {
        $errors[] = "Name is required";
    } elseif (strlen($name) > 100) {
        $errors[] = "Name too long";
    }

    if ($email === false) {
        $errors[] = "Valid email required";
    }

    if ($age === false || $age < 18) {
        $errors[] = "Must be 18+";
    }

    if (empty($errors)) {
        // process valid data
        // save to database, redirect, etc.
        header("Location: /success");
        exit;
    }
}

// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">

Сессии и cookies

Сессии хранят данные на сервере (идентифицируются cookie с ID сессии). session_start() должен вызываться до любого вывода (или используйте ob_start()). Храните минимум данных в сессиях — они потребляют память сервера. Для cookies всегда устанавливайте secure (только HTTPS), httponly (предотвращает доступ XSS) и samesite (защита CSRF). Корректно уничтожайте сессии: снимайте переменные, уничтожайте сессию, очищайте cookie. Для масштабируемых приложений используйте обработчик сессий на Redis/БД вместо файлов.

php
<?php
// start session (must be before any output)
session_start();

// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();

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

// check if logged in
function isLoggedIn(): bool {
    return isset($_SESSION['user_id']);
}

// destroy session
session_unset();     // clear variables
session_destroy();   // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie

// cookies
setcookie("theme", "dark", [
    'expires' => time() + 86400 * 30, // 30 days
    'path' => '/',
    'secure' => true,     // HTTPS only
    'httponly' => true,   // not accessible via JS
    'samesite' => 'Strict', // CSRF protection
]);

// read cookie
$theme = $_COOKIE['theme'] ?? 'light';

Файловый ввод-вывод

file_get_contents/file_put_contents удобны для маленьких файлов. Для больших файлов используйте fopen/fread/fwrite с потоками. fgetcsv/fputcsv обрабатывают формат CSV (включая кавычки/экранирование). json_decode с true возвращает ассоциативные массивы (по умолчанию объекты). Всегда проверяйте file_exists и обрабатывайте ошибки (права, диск заполнен). Для загрузки файлов используйте move_uploaded_file() для безопасности. Блокируйте файлы через flock() при параллельной записи.

php
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append

// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
    print_r($row); // array of column values
}
fclose($csv);

// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);

// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));

// file info
file_exists("data.txt");  // bool
filesize("data.txt");     // bytes
filemtime("data.txt");    // modification timestamp
is_dir("folder");         // bool

Загрузка файлов

Загрузки файлов приходят через $_FILES, а не $_POST. Всегда валидируйте: проверьте код ошибки, верифицируйте MIME-тип через finfo (не $_FILES['type'], который предоставляется клиентом и подделываем), ограничьте размер и генерируйте безопасные имена файлов (никогда не доверяйте оригинальному имени). move_uploaded_file() — функция безопасности — она проверяет, что файл был загружен через HTTP POST. Храните загрузки вне веб-корня или отдавайте через PHP для предотвращения прямого доступа. Рассмотрите сканирование загрузок на вредоносное ПО.

php
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
//   <input type="file" name="document">
//   <input type="submit">
// </form>

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['document'];

    // $file contains:
    // ['name'] => original filename
    // ['type'] => MIME type (unreliable!)
    // ['tmp_name'] => temporary path
    // ['error'] => UPLOAD_ERR_OK (0) on success
    // ['size'] => file size in bytes

    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("Upload error: " . $file['error']);
    }

    // validate
    $allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);

    if (!in_array($mimeType, $allowedTypes)) {
        die("Invalid file type");
    }

    if ($file['size'] > 5 * 1024 * 1024) { // 5MB
        die("File too large");
    }

    // move to permanent location
    $dest = "uploads/" . uniqid() . "_" . $file['name'];
    move_uploaded_file($file['tmp_name'], $dest);
    echo "Uploaded to: $dest";
}

cURL и HTTP-запросы

cURL — стандартный HTTP-клиент в PHP — обрабатывает HTTPS, перенаправления, cookies и аутентификацию. Всегда устанавливайте CURLOPT_RETURNTRANSFER для получения ответа как строки (иначе он выводится). Устанавливайте тайм-ауты во избежание зависаний. Для простых запросов file_get_contents с stream_context работает, но лишён возможностей. Для продакшена используйте Guzzle (composer require guzzlehttp/guzzle) или Symfony HTTP Client — они предлагают лучший API, логику повторов и совместимость с PSR-18.

php
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    $data = json_decode($response, true);
    print_r($data);
}

// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// simpler: file_get_contents with context
$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\n",
        'content' => json_encode(['name' => 'Alice']),
    ],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);
08

Базы данных (PDO)

Подключение PDO и основы

PDO (PHP Data Objects) — стандартный слой абстракции БД — поддерживает MySQL, PostgreSQL, SQLite и другие. Всегда устанавливайте ERRMODE_EXCEPTION для корректной обработки ошибок и ATTR_EMULATE_PREPARES=false для настоящих подготовленных запросов (лучшая безопасность). FETCH_ASSOC возвращает ассоциативные массивы (используйте FETCH_OBJ для объектов, FETCH_CLASS для маппинга на классы). Всегда используйте кодировку utf8mb4 для полной поддержки Unicode (включая эмодзи). Храните подключение в singleton или DI-контейнере.

php
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);

// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays

// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();

// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rows

Подготовленные запросы (предотвращение SQL-инъекций)

Подготовленные запросы ОБЯЗАТЕЛЬНЫ для любого запроса с пользовательским вводом — они разделяют структуру SQL и данные, делая инъекции невозможными. Используйте ? для позиционных или :name для именованных параметров. Для конструкций IN нужно динамически строить строку плейсхолдеров (но значения всё равно параметризуются). lastInsertId() возвращает последнее значение auto-increment. Никогда не конкатенируйте пользовательский ввод в SQL — даже с функциями экранирования. Это правило безопасности №1 в PHP.

php
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();

// named parameters (more readable)
$stmt = $pdo->prepare(
    "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
    ':name' => 'Alice',
    ':email' => '[email protected]',
    ':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID

// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();

// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");

Транзакции и обработка ошибок

Транзакции обеспечивают атомарность — все операции выполняются успешно или все fail. beginTransaction/commit/rollBack оборачивают единицу работы. Всегда оборачивайте транзакции в try/catch и откатывайте при любом исключении. PDO выбрасывает PDOException при ошибках (с ERRMODE_EXCEPTION). Держите транзакции короткими для уменьшения конкуренции блокировок. Для вложенных транзакций используйте savepoints или менеджер транзакций. Никогда не оставляйте транзакцию открытой — всегда commit или rollBack.

php
<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
        ->execute([$amount, $fromId]);

    $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
        ->execute([$amount, $toId]);

    // log transaction
    $pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
        ->execute([$fromId, $toId, $amount]);

    $pdo->commit();
    echo "Transfer complete";
} catch (PDOException $e) {
    $pdo->rollBack(); // undo all changes
    error_log("Transfer failed: " . $e->getMessage());
    throw new RuntimeException("Transfer failed", 0, $e);
}

// check if in transaction
if ($pdo->inTransaction()) {
    $pdo->commit();
}

Шаблоны получения данных

Выбирайте правильный режим получения для вашего случая. FETCH_ASSOC наиболее част (массив с именами столбцов). FETCH_CLASS отображает строки на объекты — отлично для доменных моделей. FETCH_KEY_PAIR создаёт map id=>value (для выпадающих списков). FETCH_GROUP группирует строки по первому столбцу — полезно для связей один-ко-многим. Для больших наборов результатов используйте fetch() в цикле вместо fetchAll() для экономии памяти. Всегда закрывайте курсоры через $stmt->closeCursor() после завершения.

php
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();

// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC);  // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM);    // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH);   // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ);    // stdClass with properties

// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);

// fetch into class
class User {
    public int $id;
    public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects

// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
    ->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']

// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
    ->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]

Лучшие практики работы с БД

Шаблон репозитория отделяет доступ к данным от бизнес-логики — делая код тестируемым (мокируйте PDO) и поддерживаемым. Используйте внедрение зависимостей для передачи подключения PDO. Никогда не создавайте новые подключения PDO на каждый запрос — переиспользуйте одно (или пул). Для высоконагруженных приложений рассмотрите пулер соединений (ProxySQL для MySQL, PgBouncer для PostgreSQL). Всегда профилируйте медленные запросы через EXPLAIN и добавляйте соответствующие индексы. Рассмотрите ORM (Doctrine, Eloquent) для сложных доменов.

php
<?php
// 1. Connection singleton (or use DI container)
class Database {
    private static ?PDO $instance = null;

    public static function conn(): PDO {
        if (self::$instance === null) {
            self::$instance = new PDO(
                "mysql:host=localhost;dbname=app;charset=utf8mb4",
                "user", "pass",
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
            );
        }
        return self::$instance;
    }
}

// 2. Repository pattern
class UserRepository {
    public function __construct(private PDO $db) {}

    public function findById(int $id): ?array {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $user = $stmt->fetch();
        return $user ?: null;
    }
}

// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queries
09

Дата/время и безопасность

Дата и время

Функции даты PHP используют часовой пояс сервера по умолчанию — всегда устанавливайте date_default_timezone_set('Asia/Shanghai') или используйте DateTimeZone явно. Класс DateTime объектно-ориентированный и лучше обрабатывает часовые пояса, интервалы и форматирование, чем процедурные функции. strtotime() разбирает английские описания дат ('next Monday', '+1 month') — удобно, но может удивить на границах месяцев. Для математики дат используйте DateTime::diff() и DateInterval. Всегда храните даты в UTC в БД.

php
<?php
// current time
$now = date('Y-m-d H:i:s');           // 2024-06-15 14:30:00
$timestamp = time();                   // Unix timestamp
$dt = new DateTime();                  // DateTime object

// formatting
echo date('Y-m-d');                    // 2024-06-15
echo date('d/m/Y H:i:s');              // 15/06/2024 14:30:00
echo date('l, F j, Y');                // Saturday, June 15, 2024

// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');

// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));

// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days;  // 365

// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));

Хэширование паролей и безопасность

password_hash() использует bcrypt (или Argon2 при наличии) с автоматической генерацией соли — никогда не делайте собственное хэширование. password_verify() безопасно проверяет пароли против хэшей (сравнение с постоянным временем для предотвращения timing-атак). password_needs_rehash() позволяет обновлять хэши при увеличении фактора стоимости. Для случайных токенов (CSRF, API-ключи, сброс пароля) всегда используйте random_bytes() — не rand() или mt_rand(), которые предсказуемы. Используйте hash_hmac для аутентификации сообщений.

php
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)

// verify password
if (password_verify($input, $hash)) {
    echo "Valid password";
}

// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // update stored hash
}

// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!

// generate secure random
$token = bin2hex(random_bytes(32));    // 64-char hex string
$bytes = random_bytes(16);             // raw bytes
$int = random_int(1, 1000);            // cryptographically secure

// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);

Экранирование вывода и предотвращение XSS

XSS — уязвимость №1 в вебе — всегда экранируйте вывод в зависимости от контекста. htmlspecialchars() для HTML (ENT_QUOTES экранирует и одинарные, и двойные кавычки). urlencode() для URL. json_encode() с hex-флагами для JavaScript-контекстов. Никогда не доверяйте пользовательскому вводу — экранируйте на выводе, а не на вводе (может потребоваться raw-данные в другом месте). Устанавливайте заголовки Content-Security-Policy как глубоко эшелонированную защиту. Рассмотрите шаблонизатор (Twig, Blade) с авто-экранированием по умолчанию.

php
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>

// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities

// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">

<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);

// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
    var data = <?= $json ?>;
</script>

<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

JSON и API-ответы

json_encode/decode — стандартные JSON-функции. Всегда устанавливайте Content-Type: application/json для API-ответов. Используйте JSON_UNESCAPED_UNICODE для сохранения китайского/эмодзи читаемым (иначе они становятся \uXXXX). json_decode с true возвращает ассоциативные массивы (чаще в PHP). Всегда проверяйте json_last_error() после декодирования ненадёжного JSON. Для REST API устанавливайте соответствующие HTTP-коды состояния (200, 201, 400, 404, 500) и используйте согласованную структуру ответа.

php
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}

// decode JSON to PHP
$obj = json_decode($json);       // stdClass object
$arr = json_decode($json, true); // associative array

// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException("JSON error: " . json_last_error_msg());
}

// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
    'status' => 'success',
    'data' => $users,
    'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);

// API request handling
$input = json_decode(file_get_contents('php://input'), true);

Composer и управление зависимостями

Composer — пакетный менеджер PHP — необходим для современного PHP. require указывает продакшен-зависимости; require-dev — для разработки (тесты и т. д.). PSR-4 автозагрузка отображает пространства имён на каталоги. Всегда коммитьте composer.json и composer.lock (фиксирует точные версии). Используйте composer install (из lock) в продакшене, composer update для получения последних. Популярные пакеты: Monolog (логирование), Guzzle (HTTP), PHPUnit (тестирование), Symfony components, Laravel framework. Никогда не коммитьте каталог vendor/.

php
<?php
// composer.json
// {
//   "require": {
//     "monolog/monolog": "^3.0",
//     "guzzlehttp/guzzle": "^7.0"
//   },
//   "autoload": {
//     "psr-4": {"App\\": "src/"}
//   }
// }

// install: composer install
// update:  composer update
// add:    composer require monolog/monolog

// autoload (at the top of your app)
require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);

// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];
10

Сессии и cookies

Установка и чтение cookies

Cookies хранят небольшие объёмы данных в браузере пользователя. setcookie() должен вызываться до любого HTML-вывода (он устанавливает HTTP-заголовки). Флаг httponly предотвращает чтение cookie через JavaScript (снижает XSS), а secure обеспечивает отправку только по HTTPS. Cookies отправляются с каждым запросом к соответствующему домену/пути, поэтому не храните большие данные. Для чувствительных данных используйте сессии (данные остаются на сервере). Всегда валидируйте и санитизируйте значения cookies — они приходят от клиента и могут быть подделаны.

php
// Set a cookie (must be before any output!)
setcookie("user", "Alice", time() + 3600, "/", "", true, true);
// params: name, value, expiry, path, domain, secure, httponly

// Read cookies (from $_COOKIE superglobal)
if (isset($_COOKIE['user'])) {
    echo "Welcome back, " . htmlspecialchars($_COOKIE['user']);
}

// Delete a cookie: set expiry in the past
setcookie("user", "", time() - 3600, "/");

// Cookie limitations:
// - Sent with every HTTP request (adds bandwidth)
// - Max 4KB per cookie, ~50 per domain
// - Stored client-side (don't store sensitive data!)
// - httponly=true prevents JavaScript access (XSS protection)
// - secure=true sends only over HTTPS

Управление сессиями

Сессии хранят данные на сервере, идентифицируемые ID сессии в cookie. В отличие от cookies, данные сессии не видны пользователю (безопаснее для чувствительных данных). session_start() должен вызываться на каждой странице, использующей сессии, до любого вывода. session_regenerate_id(true) предотвращает атаки фиксации сессии, создавая новый ID и удаляя старый — вызывайте после входа. Всегда уничтожайте сессии при выходе. Для крупных приложений храните сессии в Redis/БД вместо файлов (по умолчанию) для совместного использования между серверами.

php
// Start or resume a session (must be before output)
session_start();

// Store data in the session (lives on server, identified by session ID cookie)
$_SESSION['user_id'] = 42;
$_SESSION['username'] = 'alice';
$_SESSION['cart'] = ['item1', 'item2'];

// Read session data
if (isset($_SESSION['user_id'])) {
    echo "User: " . $_SESSION['username'];
}

// Remove a single session variable
unset($_SESSION['cart']);

// Destroy the entire session
session_unset();   // clear all variables
session_destroy(); // destroy session data on server
setcookie(session_name(), '', time() - 3600, '/'); // clear session cookie

// Regenerate ID to prevent session fixation attacks
session_regenerate_id(true);

Конфигурация сессий (php.ini)

Конфигурация безопасности сессий критична. cookie_httponly предотвращает кражу ID сессий через XSS. cookie_secure обеспечивает работу сессий только по HTTPS. SameSite=Strict предотвращает CSRF (cookie не отправляется при кросс-сайтовых запросах). use_strict_mode отклоняет неинициализированные ID сессий. gc_maxlifetime устанавливает тайм-аут неактивности. Для многосерверных развёртываний реализуйте кастомный SessionHandlerInterface для хранения сессий в БД или Redis — стандартное файловое хранилище не работает между серверами. Всегда настраивайте эти параметры в продакшене.

php
// php.ini session settings (or ini_set at runtime)
ini_set('session.cookie_lifetime', 0);     // 0 = until browser closes
ini_set('session.cookie_httponly', 1);     // prevent JS 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 IDs
ini_set('session.gc_maxlifetime', 1800);   // 30 min inactivity timeout

// Custom session handler (store in database/Redis)
class MySessionHandler implements SessionHandlerInterface {
    public function open($savePath, $sessionName) { /* connect DB */ }
    public function close() { /* close DB */ }
    public function read($id) { /* fetch from DB */ }
    public function write($id, $data) { /* save to DB */ }
    public function destroy($id) { /* delete from DB */ }
    public function gc($maxlifetime) { /* cleanup old sessions */ }
}
session_set_save_handler(new MySessionHandler(), true);
session_start();

Flash-сообщения (одноразовые уведомления)

Flash-сообщения — основанные на сессиях уведомления, показываемые один раз (например, 'Сохранено успешно!'), затем автоматически очищаемые. Шаблон: сохраните сообщение в $_SESSION при POST-запросе, затем прочитайте и снимите его при следующем GET-запросе. Это реализует шаблон Post/Redirect/Get (PRG) — после отправки формы перенаправляйте для предотвращения повторной отправки при обновлении и показывайте flash-сообщение на перенаправленной странице. Фреймворки вроде Laravel ($request->session()->flash()) и Symfony предоставляют встроенную поддержку flash-сообщений.

php
// Flash messages: shown once, then deleted
session_start();

// Set a flash message (e.g., after form submission)
$_SESSION['flash'] = [
    'type' => 'success',
    'message' => 'Item added to cart!'
];

// On the next page, display and clear:
if (isset($_SESSION['flash'])) {
    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);  // delete so it shows only once
    echo "<div class='alert alert-{$flash['type']}'>{$flash['message']}</div>";
}

// Helper function pattern
function flash(string $key, string $value = null): ?string {
    if ($value !== null) {
        $_SESSION['flash_' . $key] = $value;
        return null;
    }
    $val = $_SESSION['flash_' . $key] ?? null;
    unset($_SESSION['flash_' . $key]);
    return $val;
}

Лучшие практики безопасности сессий

Безопасность сессий требует нескольких слоев. Регенерируйте ID сессии после входа для предотвращения атак фиксации (когда атакующий устанавливает известный ID сессии). Отслеживайте last_activity для реализации тайм-аутов простоя. Опционально привязывайте сессии к IP/user-agent для обнаружения перехвата (примечание: может вызывать ложные срабатывания с мобильными сетями, меняющими IP). Никогда не храните пароли или номера кредитных карт в сессиях — храните только user ID и получайте чувствительные данные из БД при необходимости. Всегда используйте HTTPS в продакшене для предотвращения перехвата ID сессии.

php
// 1. Always start session before output
session_start();

// 2. Regenerate ID after login (prevent fixation)
if ($loginSuccessful) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user->id;
}

// 3. Validate session on each request
if (isset($_SESSION['user_id'])) {
    // Check session hasn't expired
    if (isset($_SESSION['last_activity']) &&
        time() - $_SESSION['last_activity'] > 1800) {
        session_unset();
        session_destroy();
        header('Location: /login');
        exit;
    }
    $_SESSION['last_activity'] = time();

    // Optional: verify IP/user-agent hasn't changed (anti-hijacking)
    if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        exit('Session hijacking detected');
    }
}

// 4. Use prepared statements for session storage in DB
// 5. Set secure cookie flags (see previous example)
// 6. Never store sensitive data (passwords, CC numbers) in sessions
11

Разработка REST API

Обработка JSON-запросов и ответов

REST API обмениваются JSON. В отличие от отправки форм (которые заполняют $_POST), JSON-запросы нужно читать из php://input и декодировать через json_decode. Всегда устанавливайте Content-Type: application/json для ответов и используйте http_response_code() для корректных HTTP-кодов состояния. Валидируйте весь ввод — json_decode не гарантирует ожидаемую структуру. Используйте оператор объединения с null (??) для безопасного доступа. JSON_PRETTY_PRINT полезен для отладки, но опускайте его в продакшене для меньших payload.

php
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true);  // true = associative array

if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);

// Return JSON response
header('Content-Type: application/json');
http_response_code(200);  // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
    'success' => true,
    'data' => ['id' => 1, 'name' => $name],
    'message' => 'User created'
], JSON_PRETTY_PRINT);

Роутинг и HTTP-методы

REST API отображают HTTP-методы на CRUD-операции: GET (чтение), POST (создание), PUT/PATCH (обновление), DELETE (удаление). Роутинг сопоставляет метод + URL-путь с обработчиком. Используйте preg_match для параметризованных маршрутов (например, /api/users/42). В продакшене используйте библиотеку роутера (FastRoute, Symfony Routing) или фреймворк (Laravel, Slim) для более чистого роутинга, middleware и внедрения зависимостей. Всегда возвращайте соответствующие HTTP-коды состояния: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 404 (Not Found), 500 (Server Error).

php
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');

// Route table: method => [pattern => handler]
switch (true) {
    case ($method === 'GET' && $path === 'api/users'):
        echo json_encode(getAllUsers());
        break;
    case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        echo json_encode(getUser((int)$m[1]));
        break;
    case ($method === 'POST' && $path === 'api/users'):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(createUser($data));
        http_response_code(201);
        break;
    case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(updateUser((int)$m[1], $data));
        break;
    case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        deleteUser((int)$m[1]);
        http_response_code(204);  // No Content
        break;
    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not found']);
}

Аутентификация API (JWT)

JWT обеспечивает stateless-аутентификацию — серверу не нужно хранить сессии. Токен содержит payload (user ID, срок) и подписан секретным ключом. Клиент отправляет токен в заголовке Authorization (Bearer token). Сервер проверяет подпись, чтобы убедиться, что токен не был изменён. JWT отлично подходит для API и микросервисов (не нужно разделяемое хранилище сессий). Однако JWT нельзя отозвать до истечения — используйте короткие сроки и стратегию refresh-токенов. В продакшене используйте библиотеку firebase/php-jwt, а не реализуйте криптографию сами. Никогда не храните чувствительные данные в payload JWT — он только base64-кодирован, не зашифрован.

php
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
    $user = findUserByEmail($email);
    if ($user && password_verify($password, $user['password_hash'])) {
        // Create JWT: header.payload.signature
        $header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
        $payload = base64url_encode(json_encode([
            'user_id' => $user['id'],
            'exp' => time() + 3600  // expires in 1 hour
        ]));
        $signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
        $jwt = "$header.$payload." . base64url_encode($signature);
        return $jwt;
    }
    return null;
}

// 2. Verify JWT on protected routes
function verifyJWT($token) {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return false;
    [$header, $payload, $signature] = $parts;
    $expected = base64url_encode(
        hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
    );
    if (!hash_equals($expected, $signature)) return false;
    $data = json_decode(base64url_decode($payload), true);
    return ($data['exp'] ?? 0) > time() ? $data : false;
}

// Use firebase/php-jwt library in production!

Валидация и санитизация ввода

Валидация ввода критична для безопасности API. filter_var PHP предоставляет встроенные валидаторы (FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT, FILTER_VALIDATE_URL) с опциями вроде min/max диапазона. Всегда валидируйте на сервере — клиентская валидация для UX, а не безопасности. Санитизируйте строки через htmlspecialchars для предотвращения XSS при выводе HTML. Для JSON API возвращайте 422 (Unprocessable Entity) при ошибках валидации с описательными сообщениями. Рассмотрите библиотеку валидации (Respect/Validation, Symfony Validator) для сложных правил. Никогда не доверяйте пользовательскому вводу — валидируйте тип, длину, формат и бизнес-правила.

php
// Validate and sanitize API input
function validateUserInput(array $data): array {
    $errors = [];

    // Required fields
    if (empty($data['name'])) {
        $errors[] = 'Name is required';
    }

    // Email validation
    $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
    if (!$email) {
        $errors[] = 'Valid email is required';
    }

    // Age: integer between 18 and 120
    $age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 18, 'max_range' => 120]
    ]);
    if ($age === false) {
        $errors[] = 'Age must be between 18 and 120';
    }

    // String sanitization (remove tags, trim)
    $name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES, 'UTF-8');

    // URL validation
    $website = filter_var($data['website'] ?? '', FILTER_VALIDATE_URL);

    return ['errors' => $errors, 'data' => compact('email', 'age', 'name')];
}

$result = validateUserInput($input);
if (!empty($result['errors'])) {
    http_response_code(422);  // Unprocessable Entity
    echo json_encode(['errors' => $result['errors']]);
    exit;
}

CORS (Cross-Origin Resource Sharing)

CORS контролирует, какие домены могут получить доступ к вашему API из браузера. Браузеры отправляют preflight OPTIONS-запрос для не-простых запросов (PUT/DELETE, кастомные заголовки). Ваш сервер должен ответить соответствующими заголовками Access-Control-Allow-*. Для безопасности указывайте точные origin'ы, а не '*' (особенно с учётными данными). Access-Control-Allow-Credentials: true нужен, если API использует cookies или заголовки Authorization. Vary: Origin сообщает кэшам, что ответ зависит от origin. Неправильно настроенный CORS может открыть ваш API любому сайту — всегда белый список доверенных origin.

php
// Enable CORS for API requests from browsers
header('Access-Control-Allow-Origin: https://example.com');  // specific origin
// OR: header('Access-Control-Allow-Origin: *');  // any origin (less secure)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');  // for cookies/auth
header('Access-Control-Max-Age: 86400');  // cache preflight for 24h

// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Dynamic origin (check against whitelist)
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
    header('Access-Control-Allow-Origin: ' . $origin);
    header('Vary: Origin');  // important for caching
}
12

Безопасность (XSS, CSRF, SQL-инъекции)

Предотвращение XSS (Cross-Site Scripting)

XSS возникает, когда ненадёжные данные вставляются в HTML без экранирования, позволяя атакующим выполнять JavaScript в браузерах жертв. Решение: всегда экранируйте вывод через htmlspecialchars (преобразует <, >, &, ", ' в HTML-сущности). Разные контексты требуют разного экранирования: тело HTML (htmlspecialchars), атрибуты HTML (htmlspecialchars с ENT_QUOTES), JavaScript (json_encode), URL (urlencode). Заголовки Content Security Policy (CSP) добавляют глубоко эшелонированную защиту, ограничивая источники загрузки скриптов. Никогда не используйте eval(), innerHTML или document.write() с пользовательским вводом. Фреймворки вроде Twig и Blade авто-экранируют по умолчанию.

php
// XSS: attacker injects malicious JavaScript into your page
// BAD: outputting user input without escaping
echo "<p>" . $_GET['name'] . "</p>";
// If name = <script>alert('xss')</script>, it executes!

// GOOD: escape output with htmlspecialchars
echo "<p>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";
// & < > " ' are converted to HTML entities

// For HTML attributes:
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';

// For JavaScript context (JSON encode):
echo '<script>var name = ' . json_encode($name) . ';</script>';

// For URL parameters:
echo '<a href="?q=' . urlencode($query) . '">Search</a>';

// Content Security Policy (CSP) header — defense in depth
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Use prepared statements (prevents SQL injection too)

Предотвращение CSRF (Cross-Site Request Forgery)

CSRF заставляет браузер аутентифицированного пользователя отправлять запрос на ваш сайт (например, денежный перевод) без его ведома. Защита: включайте непредсказуемый токен в формы, который атакующий не может угадать. Токен хранится в сессии и проверяется при отправке. Используйте hash_equals() для сравнения, защищённого от timing-атак. Для AJAX/API-вызовов SameSite=Strict cookies и требование кастомных заголовков (вроде X-Requested-With) обеспечивают защиту. GET-запросы никогда не должны модифицировать данные (они могут быть вызваны тегами img или ссылками). Фреймворки вроде Laravel и Symfony имеют встроенный CSRF-middleware.

php
// CSRF: attacker tricks a logged-in user into submitting a form
// to your site (using their session cookie)

// Defense: anti-CSRF tokens
session_start();

// Generate a token (once per session)
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Include token in forms as a hidden field
echo '<form method="POST" action="/transfer">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="amount">';
echo '<button type="submit">Transfer</button>';
echo '</form>';

// Verify token on POST/PUT/DELETE requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
    // Process the form...
}

// For APIs: use SameSite cookies + custom headers
// (browsers block cross-origin requests without explicit CORS permission)

Предотвращение SQL-инъекций

SQL-инъекции — уязвимость №1 в вебе — они позволяют атакующим читать/модифицировать/удалять всю вашу БД. Универсальное решение: подготовленные запросы (параметризованные запросы). Структура запроса и данные передаются раздельно, поэтому пользовательский ввод никогда не может быть интерпретирован как SQL. Никогда не конкатенируйте пользовательский ввод в запросы. prepare/execute PDO обрабатывает экранирование автоматически. Привязывайте параметры с их типами (PDO::PARAM_INT, PDO::PARAM_STR). Для конструкций IN с переменным числом элементов генерируйте плейсхолдеры динамически. Установите PDO::ATTR_EMULATE_PREPARES в false для настоящих серверных подготовленных запросов (лучшая безопасность).

php
// SQL injection: attacker manipulates queries via unescaped input
// BAD: string concatenation
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// If id = "1 OR 1=1", returns ALL users!
// If id = "1; DROP TABLE users; --", deletes the table!

// GOOD: prepared statements with parameter binding
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // real prepared statements

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Multiple parameters:
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
    ':name' => $name,
    ':email' => $email,
    ':age' => $age
]);

// IN clause with variable arguments:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

Хэширование паролей и аутентификация

password_hash() использует bcrypt (или Argon2) со случайной солью — отраслевой стандарт для хранения паролей. Соль встроена в хэш, поэтому не управляйте ею отдельно. password_verify() безопасно сравнивает ввод с сохранённым хэшем (timing-safe). password_needs_rehash() позволяет обновлять хэши при увеличении фактора стоимости или смене алгоритма — он проверяет, соответствует ли хэш текущим настройкам, и пересчитывает при следующем входе. Никогда не используйте MD5, SHA1 или plain text для паролей — они тривиально взламываются. Вводите строгую политику паролей, но предпочитайте длину сложности (NIST рекомендует минимум 8+ символов).

php
// NEVER store plain-text or MD5/SHA1 passwords!
// Use password_hash() (bcrypt/argon2 by default)

// Hash a password (on registration)
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT = bcrypt (or argon2id in PHP 7.3+)
// Store $hash in the database

// Verify a password (on login)
if (password_verify($inputPassword, $storedHash)) {
    // Password is correct
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
} else {
    echo "Invalid credentials";
}

// Rehash if algorithm was upgraded (migration)
if (password_verify($input, $hash) &&
    password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // Update database with $newHash
}

// Password requirements validation
if (strlen($password) < 8 ||
    !preg_match('/[A-Z]/', $password) ||
    !preg_match('/[a-z]/', $password) ||
    !preg_match('/[0-9]/', $password)) {
    echo "Password must be 8+ chars with upper, lower, and number";
}

Безопасность загрузки файлов

Загрузка файлов — основной вектор атак. Никогда не доверяйте $_FILES['type'] (устанавливается браузером, легко подделывается) — используйте finfo для определения реального MIME-типа. Никогда не используйте предоставленное пользователем имя файла (может содержать обход пути вроде ../../script.php) — генерируйте случайное имя. Храните загрузки вне веб-корня или в каталоге с отключённым выполнением PHP. Для изображений перекодируйте их (imagecreatefromjpeg + imagejpeg), чтобы удалить встроенный PHP-код, скрытый в EXIF-данных. Ограничьте размер файла для предотвращения отказа в обслуживании. Валидируйте расширение, MIME-тип и magic bytes. Рассмотрите сканирование загрузок антивирусом (ClamAV) для дополнительной безопасности.

php
// Secure file upload handling
$upload = $_FILES['avatar'];

// 1. Check for upload errors
if ($upload['error'] !== UPLOAD_ERR_OK) {
    die('Upload failed: error ' . $upload['error']);
}

// 2. Validate file size
$maxSize = 2 * 1024 * 1024;  // 2MB
if ($upload['size'] > $maxSize) {
    die('File too large (max 2MB)');
}

// 3. Validate MIME type (don't trust $_FILES['type']!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
    die('Invalid file type');
}

// 4. Generate a safe filename (never use user-supplied name)
$safeName = bin2hex(random_bytes(16)) . '.jpg';

// 5. Store OUTSIDE the web root (or in a non-executable dir)
$dest = __DIR__ . '/uploads/' . $safeName;
if (!move_uploaded_file($upload['tmp_name'], $dest)) {
    die('Failed to save file');
}

// 6. For images: re-encode to strip malicious metadata
$img = imagecreatefromjpeg($dest);
imagejpeg($img, $dest, 90);  // re-encode strips embedded PHP/scripts
13

cURL и HTTP-запросы

Базовый cURL GET и POST

cURL — самый мощный HTTP-клиент PHP, поддерживающий GET, POST, кастомные методы, заголовки, cookies и SSL. Всегда устанавливайте CURLOPT_RETURNTRANSFER для получения ответа как строки (иначе он выводится напрямую). CURLOPT_TIMEOUT предотвращает зависание на медленных серверах. Для POST с JSON устанавливайте заголовки Content-Type и Content-Length явно. Проверяйте curl_errno() на ошибки соединения и curl_getinfo(CURLINFO_HTTP_CODE) для HTTP-статуса. Всегда закрывайте cURL-дескрипторы через curl_close() для освобождения ресурсов. Для более простого кода рассмотрите Guzzle (обёртка cURL с более чистым API).

php
// GET request
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
    echo 'Error: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);

// POST request with JSON body
$ch = curl_init('https://api.example.com/users');
$payload = json_encode(['name' => 'Alice', 'email' => '[email protected]']);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
        'Content-Length: ' . strlen($payload)
    ],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);

cURL с аутентификацией и cookies

cURL поддерживает несколько методов аутентификации. CURLOPT_USERPWD устанавливает HTTP Basic Auth. Bearer-токены идут в заголовке Authorization. Для сессий на cookies (вход на сайт) используйте CURLOPT_COOKIEJAR для сохранения cookies и CURLOPT_COOKIEFILE для отправки при последующих запросах — это поддерживает сессию между несколькими вызовами cURL. Используйте временный файл для cookies и очищайте его через unlink(). Для API-вызовов предпочитайте токенную аутентификацию (Bearer) вместо cookies. Всегда используйте HTTPS (cURL проверяет SSL по умолчанию — не отключайте CURLOPT_SSL_VERIFYPEER в продакшене).

php
// Basic Auth
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

// Bearer token (API key)
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Accept: application/json'
]);

// Cookie-based session (login then reuse cookies)
$cookieFile = tempnam(sys_get_temp_dir(), 'cookie');

// Step 1: Login (saves cookies)
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query(['user' => 'alice', 'pass' => 'secret']),
    CURLOPT_COOKIEJAR => $cookieFile,   // save cookies here
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);

// Step 2: Access protected page (sends saved cookies)
$ch = curl_init('https://example.com/dashboard');
curl_setopt_array($ch, [
    CURLOPT_COOKIEFILE => $cookieFile,  // send cookies from here
    CURLOPT_RETURNTRANSFER => true,
]);
$dashboard = curl_exec($ch);
curl_close($ch);
unlink($cookieFile);  // cleanup

Скачивание файлов и стриминг

Для скачивания больших файлов используйте CURLOPT_FILE для записи напрямую в файловый дескриптор — это избегает загрузки всего ответа в память. CURLOPT_FOLLOWLOCATION следует за HTTP-перенаправлениями (301, 302). Для стриминга (например, данных в реальном времени) используйте CURLOPT_WRITEFUNCTION для обработки чанков по мере поступления — полезно для API, стримящих данные. CURLOPT_PROGRESSFUNCTION отслеживает прогресс загрузки/выгрузки. Устанавливайте щедрый CURLOPT_TIMEOUT для больших файлов. Для очень больших загрузок используйте CURLOPT_INFILE для стриминга из файла вместо загрузки в память. Всегда закрывайте файловые и cURL-дескрипторы для предотвращения утечек ресурсов.

php
// Download a file to disk
$ch = curl_init('https://example.com/large-file.zip');
$fp = fopen('downloaded.zip', 'w');
curl_setopt_array($ch, [
    CURLOPT_FILE => $fp,           // write directly to file
    CURLOPT_FOLLOWLOCATION => true, // follow redirects
    CURLOPT_TIMEOUT => 300,        // 5 min for large files
]);
curl_exec($ch);
fclose($fp);
curl_close($ch);

// Stream response with callback (progress monitoring)
$ch = curl_init('https://example.com/stream');
curl_setopt_array($ch, [
    CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
        echo $chunk;  // stream to output
        ob_flush();
        flush();
        return strlen($chunk);
    },
    CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
        if ($dlTotal > 0) {
            printf("
Progress: %.1f%%", ($dlNow / $dlTotal) * 100);
        }
        return 0;
    },
]);
curl_exec($ch);
curl_close($ch);

Конкурентные запросы (Multi cURL)

curl_multi_exec запускает несколько HTTP-запросов параллельно — резко быстрее последовательных запросов, когда нужны данные из нескольких эндпоинтов. Шаблон: создайте multi-дескриптор, добавьте отдельные cURL-дескрипторы, выполните multi-дескриптор в цикле (curl_multi_exec + curl_multi_select для эффективности), затем соберите результаты. Это полезно для агрегации данных из нескольких API, предзагрузки ресурсов или пакетных операций. Для более продвинутой конкурентности рассмотрите ReactPHP или Amp (асинхронные PHP-фреймворки). Учтите, что multi-cURL всё равно блокирует PHP-процесс — для настоящего async используйте циклы событий или очереди сообщений.

php
// Fetch multiple URLs in parallel (much faster than sequential)
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments'
];

$multi = curl_multi_init();
$handles = [];

// Create individual handles
foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_multi_add_handle($multi, $handles[$i]);
}

// Execute all requests concurrently
$active = null;
do {
    $status = curl_multi_exec($multi, $active);
    if ($active) {
        curl_multi_select($multi);  // wait for activity
    }
} while ($active && $status === CURLM_OK);

// Collect results
$responses = [];
foreach ($handles as $i => $ch) {
    $responses[$i] = json_decode(curl_multi_getcontent($ch), true);
    curl_multi_remove_handle($multi, $ch);
    curl_close($ch);
}
curl_multi_close($multi);

// $responses[0], [1], [2] now contain all three API responses

Использование Guzzle (современный HTTP-клиент)

Guzzle — стандартный HTTP-клиент для современного PHP — намного чище, чем raw cURL. Он предоставляет fluent API, PSR-7-совместимые объекты request/response, middleware (логирование, повтор) и асинхронные запросы через Promises. Опция 'json' авто-кодирует тело и устанавливает Content-Type. getAsync/postAsync возвращают Promises для конкурентных запросов без сложности multi-cURL. Обработка исключений встроена: RequestException ловит HTTP-ошибки (4xx, 5xx). Guzzle используется большинством фреймворков (HTTP-клиент Laravel оборачивает Guzzle). Установка через Composer: composer require guzzlehttp/guzzle.

php
// Guzzle is the most popular PHP HTTP client (composer require guzzlehttp/guzzle)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'base_uri' => 'https://api.example.com',
    'timeout'  => 30,
    'headers'  => ['Accept' => 'application/json'],
]);

// GET request
$response = $client->get('/users/42');
$body = json_decode($response->getBody(), true);
echo $response->getStatusCode();  // 200

// POST with JSON
$response = $client->post('/users', [
    'json' => ['name' => 'Alice', 'email' => '[email protected]'],
    'headers' => ['Authorization' => 'Bearer ' . $token],
]);

// Concurrent requests (Promise-based)
$promises = [
    'users' => $client->getAsync('/users'),
    'posts' => $client->getAsync('/posts'),
];
$results = GuzzleHttp\Promise\Utils::settle($promises)->wait();

// Error handling with try/catch
try {
    $response = $client->get('/nonexistent');
} catch (RequestException $e) {
    echo $e->getResponse()->getStatusCode();  // 404
}
14

Углублённое изучение DateTime

Создание и манипуляция DateTime

DateTime — надёжный класс даты/времени PHP. DateTimeImmutable предпочтительнее DateTime — он возвращает новый объект при модификации, предотвращая баги случайного изменения (критично, когда одна дата используется в нескольких местах). createFromFormat разбирает кастомные форматы. modify() принимает относительные выражения вроде '+1 week' или 'last day of next month'. Всегда указывайте часовые пояса явно для избежания поведения, зависящего от конфигурации сервера. Для математики дат (добавление интервалов) используйте DateInterval ('P1D' = 1 день, 'P2W' = 2 недели, 'PT2H' = 2 часа) с add()/sub().

php
$now = new DateTime();  // current time
$now->setTimezone(new DateTimeZone('Asia/Shanghai'));

// From string (many formats supported)
$date = new DateTime('2024-06-15 14:30:00');
$date = new DateTime('first day of next month');
$date = new DateTime('next Friday');

// From format (precise parsing)
$date = DateTime::createFromFormat('d/m/Y', '15/06/2024');

// Modify dates with relative expressions
$date->modify('+1 week');
$date->modify('-2 days');
$date->modify('last day of this month');
$date->modify('+1 year 3 months');

// Immutable version (recommended — doesn't modify original)
$dt = new DateTimeImmutable('2024-06-15');
$next = $dt->modify('+1 day');  // $dt unchanged, $next is new object
echo $dt->format('Y-m-d');   // 2024-06-15 (unchanged!)
echo $next->format('Y-m-d'); // 2024-06-16

Форматирование и часовые пояса

format() использует буквенные шаблоны для кастомизации вывода — Y (4-значный год), m (2-значный месяц), d (2-значный день), H (24-часовой), i (минуты), s (секунды). Для ISO 8601 (используется в API) применяйте 'Y-m-d\TH:i:sP' или сокращение 'c'. Конвертация часового пояса: создайте с исходным часовым поясом, затем setTimezone для конвертации. Всегда храните даты в UTC в БД и конвертируйте в часовой пояс пользователя только для отображения. База часовых поясов PHP исчерпывающая (включает правила DST). Используйте DateTimeZone::listIdentifiers() для получения всех поддерживаемых зон. Символ экранирования 'T' в format() выводит литеральную 'T' (для ISO 8601).

php
// Format codes (most common):
// Y=2024  y=24  m=06  n=6  d=15  j=15
// H=14 (24h)  h=02 (12h)  i=30  s=00  A=PM  a=pm
// D=Mon  l=Monday  M=Jun  F=June  N=1 (Mon=1..Sun=7)

$date = new DateTime('2024-06-15 14:30:00');
echo $date->format('Y-m-d H:i:s');  // 2024-06-15 14:30:00
echo $date->format('l, F j, Y');    // Saturday, June 15, 2024
echo $date->format('Y-m-d\TH:i:sP'); // 2024-06-15T14:30:00+08:00 (ISO 8601)

// Timezone conversion
$utc = new DateTime('2024-06-15 14:00:00', new DateTimeZone('UTC'));
$utc->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $utc->format('H:i');  // 22:00 (UTC+8)

// List supported timezones
$tzs = DateTimeZone::listIdentifiers();
// ['UTC', 'America/New_York', 'Asia/Shanghai', ...]

// Get timezone offset
$tz = new DateTimeZone('America/Los_Angeles');
$offset = $tz->getOffset(new DateTime());  // seconds from UTC

Интервалы дат и разницы

DateInterval представляет временную длительность в формате ISO 8601 (P1Y2M3DT4H5M6S). add() и sub() применяют интервалы к датам. diff() возвращает DateInterval, представляющий разницу между двумя датами — свойство 'days' даёт общее количество дней, а 'y', 'm', 'd' — компонентную разбивку. Свойство 'invert' указывает направление (1, если вторая дата раньше). Будьте осторожны с арифметикой месяцев: добавление 'P1M' к 31 января даёт 2 марта (в феврале 28-29 дней), а не 31 февраля. Для расчётов рабочих дней итерируйте и пропускайте выходные/праздники вручную или используйте библиотеку вроде nesbot/carbon.

php
// DateInterval: represents a duration
$interval = new DateInterval('P1Y2M3DT4H5M6S');
// P = period, 1Y = 1 year, 2M = 2 months, 3D = 3 days
// T = time separator, 4H = 4 hours, 5M = 5 minutes, 6S = 6 seconds

$date = new DateTime('2024-06-15');
$date->add(new DateInterval('P1M'));  // +1 month → 2024-07-15
$date->sub(new DateInterval('P10D')); // -10 days → 2024-07-05

// Difference between two dates
$d1 = new DateTime('2024-01-01');
$d2 = new DateTime('2024-12-31');
$diff = $d1->diff($d2);
echo $diff->days;     // 365 (total days)
echo $diff->m;        // 0 (months component)
echo $diff->y;        // 0 (years component)
echo $diff->format('%a days');  // 365 days
echo $diff->format('%y years, %m months, %d days');  // 0 years, 11 months, 30 days

// Check if inverted (d2 < d1)
echo $diff->invert;   // 0 if d2 >= d1, 1 if d2 < d1

DatePeriod (итерация диапазонов дат)

DatePeriod итерирует по диапазону дат с заданным интервалом — идеально для генерации календарей, отчётов или повторяющихся событий. Конструктор принимает (start, interval, end) или (start, interval, recurrences). Конечная дата исключается. Частые случаи: генерация всех дней месяца для календарного представления, перечисление платёжных периодов или создание расписаний повторяющихся событий. Используйте iterator_to_array() для материализации периода в массив. Для сложных правил повторения (например, 'каждый второй вторник') рассмотрите специализированную библиотеку вроде rrule (правила повторения RFC 5545).

php
// DatePeriod iterates over a range of dates
$start = new DateTime('2024-06-01');
$end = new DateTime('2024-06-10');
$interval = new DateInterval('P1D');  // 1 day

$period = new DatePeriod($start, $interval, $end);
foreach ($period as $day) {
    echo $day->format('Y-m-d (D)') . "\n";
}
// 2024-06-01 (Sat)
// 2024-06-02 (Sun)
// ... through 2024-06-09 (end is exclusive)

// Generate next 12 months
$start = new DateTime('first day of this month');
$interval = new DateInterval('P1M');
$period = new DatePeriod($start, $interval, 12);  // 12 recurrences
foreach ($period as $month) {
    echo $month->format('F Y') . "\n";
}

// Every Monday for a year
$start = new DateTime('next Monday');
$end = new DateTime('+1 year');
$period = new DatePeriod($start, new DateInterval('P1W'), $end);
$mondays = iterator_to_array($period);

Библиотека Carbon (расширенный DateTime)

Carbon расширяет DateTime с fluent, выразительным API — это де-факто стандарт в экосистеме PHP (используется Laravel). diffForHumans() выдаёт '5 дней назад', 'через 3 часа' — идеально для UI-временных меток. Fluent API цепочечно вызывает методы (addYear()->subMonth()->endOfMonth()). Методы сравнения (isWeekend, isPast, isToday) упрощают частые проверки. Локализация поддерживает 50+ языков для человекочитаемого вывода. Carbon 3 (2024+) неизменяем по умолчанию. Установка через Composer: composer require nesbot/carbon. Если вы используете Laravel, Carbon уже включён.

php
// Carbon: the most popular DateTime library (composer require nesbot/carbon)
use Carbon\Carbon;

$now = Carbon::now('Asia/Shanghai');
$tomorrow = Carbon::tomorrow();
$lastWeek = Carbon::now()->subWeek();

// Human-readable differences
echo Carbon::now()->diffForHumans(Carbon::now()->subDays(5));
// "5 days ago"
echo Carbon::now()->addHours(3)->diffForHumans();
// "3 hours from now"

// Fluent API
$date = Carbon::create(2024, 6, 15, 14, 30, 0)
    ->addYear()
    ->subMonth()
    ->endOfMonth()
    ->setTimezone('UTC');

// Comparison methods
if ($date->isWeekend()) { echo "Weekend!"; }
if ($date->isPast()) { echo "Past"; }
if ($date->isFuture()) { echo "Future"; }
if ($date->isToday()) { echo "Today"; }
if ($date->isLeapYear()) { echo "Leap year"; }

// Localization
Carbon::setLocale('zh');
echo Carbon::now()->subDay()->diffForHumans();  // "1天前"
15

Пространства имён и автозагрузка

Основы пространств имён

Пространства имён организуют код в иерархические пакеты, предотвращая коллизии имён классов между библиотеками. Объявление namespace должно быть первым оператором (после declare()). Оператор 'use' импортирует классы из других пространств имён — помещайте use-операторы в начало файла. Алиасы (as) разрешают конфликты, когда у двух классов одинаковое имя. Ведущий обратный слэш (\DateTime) ссылается на глобальное пространство имён. Пространства имён PHP используют обратные слэши (\) как разделители, отображаясь на структуру каталогов при PSR-4 автозагрузке. Групповые use-операторы (use App\Models\{User, Post}) уменьшают шаблонность.

php
<?php
// Namespaces prevent name collisions (like packages in Java)
namespace App\Services;

class UserService {
    public function find($id) { /* ... */ }
}

// Using namespaced classes
use App\Services\UserService;
use App\Models\User;

$service = new UserService();
$user = new User();

// Aliasing (resolve conflicts or shorten names)
use App\Services\UserService as USvc;
use App\Models\User as UserModel;

// Global namespace (backslash prefix)
$now = new \DateTime();  // \ means root/global namespace
$array = new \ArrayObject();

// Multiple use statements grouped
use App\Models\{User, Post, Comment};
use App\Services\{UserService, PostService};

Стандарт автозагрузки PSR-4

PSR-4 — стандартная спецификация автозагрузки — она отображает пространства имён на пути к каталогам, поэтому вам никогда не нужны ручные require/include. Правило: App\Services\UserService отображается на src/Services/UserService.php (App\ → src/). Настройте отображение в секции autoload composer.json. После добавления новых классов выполните 'composer dump-autoload' для регенерации карты классов. Файл vendor/autoload.php (генерируется Composer) обрабатывает загрузку — включите его один раз в точке входа (index.php). PSR-4 требует, чтобы имена классов совпадали с именами файлов (UserService → UserService.php).

php
// PSR-4: namespace structure maps to file paths
// App\Services\UserService → src/Services/UserService.php

// composer.json PSR-4 configuration:
// {
//   "autoload": {
//     "psr-4": {
//       "App\\": "src/"
//     }
//   }
// }

// File: src/Services/UserService.php
namespace App\Services;

class UserService {
    public function getUser($id) {
        return "User $id";
    }
}

// File: src/Models/User.php
namespace App\Models;

class User {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
}

// After adding classes, regenerate autoloader:
// $ composer dump-autoload

// The autoloader is included once in your entry point:
require __DIR__ . '/vendor/autoload.php';

$service = new App\Services\UserService();
$user = new App\Models\User('Alice');

Автозагрузка без Composer (spl_autoload)

spl_autoload_register регистрирует функцию, вызываемую, когда класс ещё не загружен — она получает полное имя класса и должна require соответствующий файл. Можно зарегистрировать несколько автозагрузчиков (они вызываются по порядку). Это то, что использует Composer внутри. Для продакшена всегда используйте PSR-4-автозагрузчик Composer — он оптимизирован, обрабатывает краевые случаи и генерирует карты классов для более быстрого поиска. Используйте spl_autoload_register напрямую только для крошечных проектов или когда Composer недоступен. Параметр 'true' в class_exists() запускает автозагрузку, если класс не загружен.

php
<?php
// Simple autoloader for small projects (no Composer needed)
spl_autoload_register(function ($className) {
    // Convert namespace separators to directory separators
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $className) . '.php';

    // App\Services\UserService → src/App/Services/UserService.php
    // Adjust prefix mapping as needed:
    $file = str_replace('App/', '', $file);  // remove 'App' prefix

    if (file_exists($file)) {
        require $file;
    }
});

// Now classes are loaded automatically on first use
$service = new App\Services\UserService();

// Multiple autoloaders (called in order until one loads the class)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/lib/' . $class . '.php';
    if (file_exists($path)) require $path;
});

// Check if a class is autoloadable
if (class_exists('App\Helper', true)) {  // true = attempt autoload
    $helper = new App\Helper();
}

Константы и функции пространств имён

Пространства имён могут содержать константы и функции, а не только классы. Импортируйте их через 'use const' и 'use function' (PHP 5.6+). Это полезно для конфигурационных констант и утилитарных функций. Неквалифицированные вызовы функций/констант имеют fallback-поведение: PHP сначала ищет в текущем пространстве имён, затем возвращается к глобальному. Поэтому можно вызывать strlen() без обратного слэша — но для производительности и ясности префиксируйте глобальные функции через \ в коде с пространствами имён. Групповые импорты (use App\Config\{const DB_HOST, function connect}) уменьшают многословность.

php
<?php
namespace App\Config;

// Constants in namespaces
const DB_HOST = 'localhost';
const DB_PORT = 3306;

// Functions in namespaces
function connect() {
    return 'Connected to ' . DB_HOST;
}

// Using namespaced constants and functions
use const App\Config\DB_HOST;
use function App\Config\connect;

echo DB_HOST;      // localhost
echo connect();    // Connected to localhost

// Or with fully-qualified names:
echo \App\Config\DB_HOST;
echo \App\Config\connect();

// Namespace-level use for multiple imports
use App\Config\{const DB_HOST, const DB_PORT, function connect};

// Fallback: unqualified function calls fall back to global
// if not found in current namespace
namespace App;
$len = strlen('hello');  // calls global strlen() (fallback)

Анонимные классы и автозагрузка

Анонимные классы (PHP 7+) позволяют создавать простые одноразовые объекты без определения именованного класса — полезно для интерфейсов, mock-объектов и callback'ов. Они могут реализовывать интерфейсы, расширять классы, иметь конструкторы и использовать trait. Класс генерируется во время выполнения с авто-сгенерированным именем (class@anonymous). Анонимные классы загружаются немедленно (автозагрузка не нужна). Используйте их для: простых паттернов strategy, test doubles/mock'ов, event listener'ов и DTO. Для переиспользуемых классов всегда определяйте именованные классы с корректной PSR-4 автозагрузкой. Анонимные классы особенно удобны в тестах для создания лёгких stub'ов.

php
<?php
namespace App\Factory;

// Anonymous class (PHP 7+): create a one-off class inline
interface Logger {
    public function log(string $msg): void;
}

class App {
    private $logger;
    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
}

$app = new App();
$app->setLogger(new class implements Logger {
    public function log(string $msg): void {
        echo "[LOG] $msg\n";
    }
});

// Anonymous class with constructor
$comparator = new class($ascending = true) {
    private $asc;
    public function __construct(bool $asc) { $this->asc = $asc; }
    public function compare($a, $b): int {
        return $this->asc ? $a <=> $b : $b <=> $a;
    }
};

// Get the auto-generated class name
echo get_class($comparator);  // class@anonymous...
16

Углублённое ООП (Trait, интерфейсы, abstract)

Абстрактные классы и методы

Абстрактные классы предоставляют базу с общей реализацией, которую расширяют подклассы. Их нельзя инстанцировать напрямую. Абстрактные методы определяют контракт (только сигнатура), который должны реализовать конкретные подклассы — это 'паттерн шаблонный метод'. В отличие от интерфейсов, абстрактные классы могут иметь свойства, конструкторы и конкретные методы. Используйте абстрактные классы, когда подклассы имеют значительную общую реализацию (отношение 'is-a'). Используйте интерфейсы, когда нужен просто контракт, который может реализовать любой класс (отношение 'can-do'). Класс может расширять только один абстрактный класс, но реализовывать несколько интерфейсов.

php
<?php
// Abstract class: can't be instantiated, may have abstract methods
abstract class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // Abstract method: must be implemented by subclasses
    abstract public function makeSound(): string;

    // Concrete method: shared implementation
    public function describe(): string {
        return $this->name . " says " . $this->makeSound();
    }
}

class Dog extends Animal {
    public function makeSound(): string {
        return "Woof!";
    }
}

class Cat extends Animal {
    public function makeSound(): string {
        return "Meow!";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof!
// new Animal("test");  // Error: cannot instantiate abstract class

Интерфейсы и множественная реализация

Интерфейсы определяют контракт — сигнатуры методов без реализации. Класс может реализовывать несколько интерфейсов (в отличие от одиночного наследования классов). Интерфейсы обеспечивают полиморфизм: любой класс, реализующий Comparable, может быть отсортирован, независимо от конкретного типа. Используйте интерфейсы для определения возможностей (Comparable, Serializable, Iterable), пересекающих иерархии классов. Type hinting с интерфейсами (function sort(Comparable $a)) гибче, чем с конкретными классами. Наследование интерфейсов (interface A extends B, C) объединяет контракты. Современный PHP также поддерживает константы интерфейсов и статические методы в интерфейсах.

php
<?php
// Interface: pure contract (no implementation)
interface Comparable {
    public function compareTo($other): int;
}

interface Serializable {
    public function serialize(): string;
    public function unserialize(string $data): void;
}

// A class can implement MULTIPLE interfaces
class Product implements Comparable, Serializable {
    private $price;

    public function __construct(float $price) {
        $this->price = $price;
    }

    public function compareTo($other): int {
        return $this->price <=> $other->price;  // spaceship operator
    }

    public function serialize(): string {
        return serialize($this->price);
    }

    public function unserialize(string $data): void {
        $this->price = unserialize($data);
    }
}

// Type hinting with interfaces
function sortItems(array $items): array {
    usort($items, fn($a, $b) => $a->compareTo($b));
    return $items;
}

// Interface inheritance
interface Repository extends Comparable, Serializable {
    public function find(int $id): ?object;
}

Trait (переиспользование кода без наследования)

Trait обеспечивает горизонтальное переиспользование кода — методы, которые можно 'вставить' в любой класс без наследования. Это решает проблему ромба (в PHP одиночное наследование). Частые применения trait: логирование, паттерн singleton, soft deletes, временные метки. Класс может использовать несколько trait. Когда у trait конфликтующие методы, используйте 'insteadof' для выбора одного и 'as' для алиаса другого. Trait могут иметь абстрактные методы (заставляя использующий класс реализовать их) и статические методы/свойства. Будьте осторожны, не злоупотребляйте trait — они могут усложнить трассировку кода. Предпочитайте композицию (внедрение зависимостей) trait для сложного поведения.

php
<?php
// Trait: reusable method groups (PHP's answer to multiple inheritance)
trait Logger {
    protected function log(string $msg, string $level = 'INFO'): void {
        echo "[$level] " . date('Y-m-d H:i:s') . " $msg\n";
        // In real code: write to file/database
    }
}

trait Singleton {
    private static $instance;
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

class UserService {
    use Logger, Singleton;

    public function findUser($id) {
        $this->log("Finding user $id");
        return "User $id";
    }
}

$svc = UserService::getInstance();
$svc->findUser(42);  // [INFO] 2024-06-15 14:30:00 Finding user 42

// Conflict resolution when traits have same method
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
    use A, B {
        B::hello insteadof A;  // use B's hello
        A::hello as helloFromA; // alias A's as helloFromA
    }
}

Позднее статическое связывание (static:: vs self::)

Позднее статическое связывание (LSB) — разница между self:: (время компиляции, всегда ссылается на определяющий класс) и static:: (время выполнения, ссылается на вызывающий класс). Это важно в наследовании: если у Base есть метод, использующий self::$table, он всегда видит $table Base, даже когда вызван на Child. Использование static::$table заставляет его видеть $table Child. LSB необходим для паттернов factory (new static() создаёт экземпляры вызываемого класса), ActiveRecord (каждая модель имеет свою таблицу) и паттерна singleton. Возвращаемый тип 'static' (PHP 8+) объявляет, что метод возвращает экземпляр вызываемого класса.

php
<?php
class Base {
    protected static $table = 'base';

    public static function getTable(): string {
        // self:: refers to the class where the method is DEFINED
        return self::$table;  // always 'base'
    }

    public static function getTableStatic(): string {
        // static:: refers to the class that was CALLED (runtime)
        return static::$table;  // late static binding
    }

    public static function create(): static {
        // 'static' return type + new static() = factory pattern
        return new static();
    }
}

class Child extends Base {
    protected static $table = 'child';
}

echo Child::getTable();        // 'base' (self:: = Base)
echo Child::getTableStatic();  // 'child' (static:: = Child)
echo get_class(Child::create()); // 'Child' (new static = Child)

// self:: is resolved at compile time (the defining class)
// static:: is resolved at runtime (the calling class)
// This is "Late Static Binding" — essential for factory patterns

Магические методы

Магические методы — специальные методы, перехватывающие операции с объектами. __get/__set создают динамические свойства (полезно для объектов передачи данных, ORM). __toString позволяет echo $object. __invoke делает объект вызываемым как функцию. __isset/__unset поддерживают isset()/unset() на динамических свойствах. __debugInfo кастомизирует вывод var_dump. Другие магические методы: __construct, __destruct, __clone (для глубокого клонирования), __call/__callStatic (для неопределённых методов, включает fluent API и mixin'ы), __serialize/__unserialize (заменяет __sleep/__wakeup в PHP 7.4+). Используйте магические методы умеренно — они добавляют 'магическое' поведение, которое трудно отлаживать. Чётко документируйте их.

php
<?php
class MagicBox {
    private $data = [];

    // Called when accessing undefined properties
    public function __get($name) {
        return $this->data[$name] ?? null;
    }

    // Called when setting undefined properties
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    // Called when isset() or empty() on undefined property
    public function __isset($name): bool {
        return isset($this->data[$name]);
    }

    // Called when unset() on undefined property
    public function __unset($name): void {
        unset($this->data[$name]);
    }

    // Called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // Called when object is called as function
    public function __invoke($arg) {
        return "Called with $arg";
    }

    // Called for var_dump/debugging
    public function __debugInfo(): array {
        return ['keys' => array_keys($this->data)];
    }
}

$box = new MagicBox();
$box->name = "Alice";       // __set
echo $box->name;            // __get → Alice
echo $box;                  // __toString → {"name":"Alice"}
echo $box("test");          // __invoke → Called with test
17

Управление пакетами Composer

Основы composer.json

composer.json — манифест для PHP-проектов. require перечисляет продакшен-зависимости с ограничениями версий (^ разрешает минорные обновления, ~ разрешает патчи). autoload определяет PSR-4 отображение пространств имён на каталоги. require-dev содержит зависимости только для разработки. Запустите composer install для настройки проекта.

php
{
    "name": "myorg/myapp",
    "type": "project",
    "require": {
        "php": ">=8.1",
        "monolog/monolog": "^3.0",
        "symfony/console": "^7.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": { "MyApp\\": "src/" }
    }
}

Установка и обновление

composer install читает composer.lock для точных версий (воспроизводимые сборки). composer require добавляет пакет и разрешает зависимости. composer update получает более новые версии в рамках ограничений. Используйте --no-dev для продакшена. --optimize-autoloader преобразует PSR-4 в classmap для более быстрой автозагрузки в продакшене.

php
# Install all dependencies from composer.lock
composer install

# Add a package (modifies composer.json)
composer require monolog/monolog
composer require --dev phpunit/phpunit

# Update all packages to latest allowed versions
composer update

# Update a single package
composer update monolog/monolog

# Production install (no dev, optimized autoloader)
composer install --no-dev --optimize-autoloader

# Show installed packages
composer show

Ограничения версий

Карет (^) — наиболее частое ограничение: разрешает изменения, не модифицирующие крайнюю левую ненулевую цифру. Тильда (~) фиксирует на уровне патча. Для версий 0.x, ^0.3 разрешает 0.3.x, но не 0.4. Всегда используйте ограничения для получения security-патчей, избегая ломающих изменений. Фиксируйте точные версии в composer.lock для воспроизводимости.

php
"require": {
    // Caret: >=1.2.0, <2.0.0 (allows minor+patch)
    "vendor/pkg": "^1.2",

    // Tilde: >=1.2.0, <1.3.0 (patch only)
    "vendor/pkg2": "~1.2",

    // Exact version
    "vendor/pkg3": "1.2.3",

    // Range
    "vendor/pkg4": ">=1.0 <2.0",

    // Wildcard
    "vendor/pkg5": "1.2.*",

    // Stability flags
    "vendor/pkg6": "dev-main",
    "vendor/pkg7": "2.0@beta"
}

PSR-4 автозагрузка

PSR-4 автозагрузка отображает префиксы пространств имён на каталоги: MyApp\Services\UserService разрешается в src/Services/User.php. Запустите composer dump-autoload после добавления новых классов. Для продакшена используйте --optimize для генерации classmap (один поиск в массиве вместо проверок файловой системы). Classmap-автозагрузка сканирует каталоги и является самой быстрой для фиксированных кодовых баз.

php
// PSR-4: "MyApp\\": "src/" means
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php

namespace MyApp\Services;

class UserService
{
    public function find(int $id): ?User
    {
        // ...
    }
}

// After running composer dump-autoload:
// require 'vendor/autoload.php';
// $service = new \MyApp\Services\UserService();

// Classmap (faster for production)
// "autoload": { "classmap": ["src/", "lib/"] }

Скрипты и хуки

Composer scripts определяют проектно-специфичные команды. Запуск через composer <имя>. Встроенные события (post-install-cmd, post-update-cmd, pre-autoload-dump) срабатывают автоматически. Скрипты могут ссылаться на другие скрипты через @name. Используйте скрипты для стандартизации процессов разработки в команде.

php
{
    "scripts": {
        "test": "phpunit",
        "lint": "phpcs --standard=PSR12 src/",
        "fix": "phpcbf --standard=PSR12 src/",
        "post-install-cmd": [
            "MyApp\\Setup::postInstall",
            "php artisan migrate"
        ],
        "post-update-cmd": "@post-install-cmd"
    }
}

// Run: composer test, composer lint, etc.
18

Продвинутый cURL

Multi-запросы (параллельно)

curl_multi_exec запускает несколько запросов параллельно, резко сокращая общее время для пакетных API-вызовов. curl_multi_select блокирует до появления активности, избегая busy-waiting. Всегда закрывайте дескрипторы и multi-дескриптор для освобождения ресурсов. Это основа высокопроизводительного HTTP-скрейпинга и API-агрегации.

php
<?php
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments',
];

$multi = curl_multi_init();
$handles = [];

foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_setopt($handles[$i], CURLOPT_TIMEOUT, 10);
    curl_multi_add_handle($multi, $handles[$i]);
}

do {
    $status = curl_multi_exec($multi, $active);
    if ($active) curl_multi_select($multi);
} while ($active && $status === CURLM_OK);

foreach ($handles as $i => $ch) {
    $responses[$i] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);

Стриминговые ответы

CURLOPT_WRITEFUNCTION предоставляет callback для каждого чанка ответа, обеспечивая потоковую обработку больших файлов без полной загрузки в память. Возвращайте длину чанка для сигнализации потребления. Это необходимо для скачивания больших файлов, обработки стриминговых API или инкрементального парсинга CSV/JSON.

php
<?php
$ch = curl_init('https://example.com/large-file.csv');
$file = fopen('download.csv', 'w');

curl_setopt($ch, CURLOPT_FILE, $file);  // Write to file
// OR use callback for streaming processing:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($file) {
    fwrite($file, $chunk);
    // Or parse incrementally:
    // $lines = explode("\n", $chunk);
    return strlen($chunk);  // Must return bytes consumed
});

curl_exec($ch);
fclose($file);
curl_close($ch);

Аутентификация и cookies

Устанавливайте кастомные заголовки через CURLOPT_HTTPHEADER для аутентификации (Bearer-токены, API-ключи). COOKIEJAR/COOKIEFILE сохраняют cookies между запросами для аутентификации на сессиях. CURLOPT_USERPWD устанавливает HTTP Basic Auth. Для POST устанавливайте CURLOPT_POSTFIELDS с JSON и заголовком Content-Type. Всегда устанавливайте Accept для контроля формата ответа.

php
<?php
$ch = curl_init('https://api.example.com/data');

// Bearer token
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
]);

// Cookie jar (persist across requests)
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');   // Save
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');  // Load

// Basic auth
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');

// POST with JSON body
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['key' => 'value']));

$response = curl_exec($ch);

Обработка ошибок и повторы

Всегда проверяйте возвращаемое значение curl_exec (false при неудаче) и curl_error для сообщения. curl_getinfo предоставляет код состояния HTTP, тайминги и информацию о перенаправлениях. Реализуйте экспоненциальный откат для повторов, чтобы обрабатывать лимиты запросов и временные сбои. Различайте сетевые ошибки (ошибка curl) и HTTP-ошибки (код состояния).

php
<?php
function fetchWithRetry(string $url, int $max = 3): ?string
{
    for ($attempt = 1; $attempt <= $max; $attempt++) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response !== false && $code >= 200 && $code < 300) {
            return $response;
        }

        if ($attempt < $max) {
            sleep(pow(2, $attempt));  // Exponential backoff
        }
    }
    throw new RuntimeException("Failed: {$error}");
}

Справочник опций cURL

CURLOPT_FOLLOWLOCATION следует за HTTP-перенаправлениями (3xx). Всегда держите SSL_VERIFYPEER true в продакшене для предотвращения MITM-атак; скачайте cacert.pem с curl.haxx.se. CURLOPT_ENCODING включает сжатие. Используйте CURLOPT_VERBOSE с STDERR для отладки проблем соединения. Устанавливайте разумные тайм-ауты во избежание зависаний.

php
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  // Return not echo
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);   // Follow redirects
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);

// SSL (keep VERIFYPEER true in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');

// Performance
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');

// Debug
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('curl.log', 'w'));
19

Обработка изображений (GD)

Создание и загрузка изображений

imagecreatetruecolor создаёт true color изображение (миллионы цветов). imagecolorallocate регистрирует цвет и возвращает идентификатор. imagecreatefromjpeg/png/webp загружает существующие файлы. Всегда проверяйте возвращаемое значение (false при неудаче). Используйте imagesx/imagesy для получения размеров. Освобождайте память через imagedestroy после завершения.

php
<?php
// Create a blank image
$img = imagecreatetruecolor(400, 300);

// Allocate colors
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);

// Fill background
imagefill($img, 0, 0, $white);

// Load existing images
$photo = imagecreatefromjpeg('photo.jpg');
$png = imagecreatefrompng('logo.png');
$webp = imagecreatefromwebp('image.webp');

// Get dimensions
$width = imagesx($photo);
$height = imagesy($photo);

Рисование фигур и текста

GD предоставляет примитивы рисования: прямоугольники, эллипсы, линии, многоугольники и дуги. Залитые варианты (imagefilled*) рисуют сплошные фигуры. imagettftext рендерит TrueType-шрифты с контролем угла и размера. Всегда отправляйте заголовок Content-Type перед выводом данных изображения. Вызывайте imagedestroy для освобождения памяти.

php
<?php
$img = imagecreatetruecolor(400, 300);
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
$blue = imagecolorallocate($img, 0, 0, 255);
imagefill($img, 0, 0, $white);

// Shapes
imagerectangle($img, 50, 50, 150, 120, $red);
imagefilledrectangle($img, 200, 50, 300, 120, $blue);
imageellipse($img, 100, 200, 80, 80, $red);
imageline($img, 0, 0, 400, 300, $red);

// Text with TrueType font
imagettftext($img, 20, 0, 50, 270, $red, 'arial.ttf', 'Hello GD!');

header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);

Изменение размера и обрезка

imagecopyresampled даёт более качественные результаты, чем imagecopyresized (использует интерполяцию). Сохраняйте пропорции, вычисляя размеры из оригинала. Для миниатюр центрируйте-обрезайте в квадрат для согласованного макета. Всегда уничтожайте исходные изображения после копирования для предотвращения утечек памяти при пакетной обработке.

php
<?php
function resizeImage($src, $maxW, $maxH) {
    list($w, $h) = getimagesize($src);
    $ratio = min($maxW / $w, $maxH / $h);
    $newW = (int)($w * $ratio);
    $newH = (int)($h * $ratio);

    $srcImg = imagecreatefromjpeg($src);
    $dstImg = imagecreatetruecolor($newW, $newH);

    // High-quality resampling
    imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0,
        $newW, $newH, $w, $h);

    imagedestroy($srcImg);
    return $dstImg;
}

// Center crop to square
function cropSquare($src, $size) {
    list($w, $h) = getimagesize($src);
    $min = min($w, $h);
    $x = (int)(($w - $min) / 2);
    $y = (int)(($h - $min) / 2);
    $dst = imagecreatetruecolor($size, $size);
    $img = imagecreatefromjpeg($src);
    imagecopyresampled($dst, $img, 0, 0, $x, $y, $size, $size, $min, $min);
    return $dst;
}

Фильтры и эффекты

imagefilter применяет встроенные эффекты: оттенки серого, яркость (диапазон от -255 до 255), контраст (отрицательный увеличивает), размытие, выделение краёв, инверсия и цветовая окраска (RGB + alpha). Pixelate создаёт эффект мозаики. Они быстрые, но базовые; для продвинутых эффектов используйте ImageMagick (расширение Imagick), поддерживающее матрицы свёртки и кастомные фильтры.

php
<?php
$img = imagecreatefromjpeg('photo.jpg');

// Built-in filters
imagefilter($img, IMG_FILTER_GRAYSCALE);      // B&W
imagefilter($img, IMG_FILTER_BRIGHTNESS, 30);  // Brighten
imagefilter($img, IMG_FILTER_CONTRAST, -20);   // More contrast
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);   // Blur
imagefilter($img, IMG_FILTER_EDGEDETECT);      // Edge detection
imagefilter($img, IMG_FILTER_NEGATE);          // Invert colors

// Colorize (tint)
imagefilter($img, IMG_FILTER_COLORIZE, 0, 0, 100, 0);  // Blue tint

// Pixelate
imagefilter($img, IMG_FILTER_PIXELATE, 10, true);

imagepng($img, 'filtered.png');
imagedestroy($img);

Водяные знаки и композитинг

imagecopymerge накладывает одно изображение на другое с регулируемой непрозрачностью (0-100). PNG-водяные знаки с alpha-каналами смешиваются естественно. Для текстовых водяных знаков используйте imagecolorallocatealpha для полупрозрачного текста. Качество imagejpeg варьируется от 0 (худшее) до 100 (лучшее); 75-90 — хороший баланс для веба. Всегда уничтожайте оба изображения.

php
<?php
$photo = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');

$pw = imagesx($photo); $ph = imagesy($photo);
$ww = imagesx($watermark); $wh = imagesy($watermark);

// Position: bottom-right with 20px padding
$destX = $pw - $ww - 20;
$destY = $ph - $wh - 20;

// Merge with 50% opacity
imagecopymerge($photo, $watermark, $destX, $destY, 0, 0, $ww, $wh, 50);

// Text watermark
$color = imagecolorallocatealpha($photo, 255, 255, 255, 60);
imagettftext($photo, 30, 0, 20, $ph - 20, $color, 'arial.ttf', '© 2025');

imagejpeg($photo, 'watermarked.jpg', 90);  // 90% quality
imagedestroy($photo);
imagedestroy($watermark);
20

Углублённые сессии и cookies

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

Безопасные сессии требуют: HttpOnly cookies (без доступа через JavaScript), флаг Secure (только HTTPS), SameSite=Strict (защита CSRF) и strict mode (отклонение неинициализированных ID сессий). Всегда регенерируйте ID сессии после изменения привилегий (вход, административный доступ) для предотвращения фиксации сессии. Используйте кастомное имя сессии, чтобы не раскрывать PHP.

php
<?php
// php.ini or runtime configuration
ini_set('session.cookie_httponly', 1);    // No JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);     // Reject uninitialized IDs
ini_set('session.gc_maxlifetime', 3600);   // 1 hour

session_name('APP_SID');  // Custom name
session_start();

// Regenerate ID after login (prevent fixation)
session_regenerate_id(true);

$_SESSION['user_id'] = 123;
$_SESSION['login_time'] = time();

Кастомный обработчик сессий

Кастомные обработчики сессий хранят данные сессий в БД, Redis или Memcached вместо файлов. Реализуйте SessionHandlerInterface с методами open, close, read, write, destroy и gc. Хранение в БД обеспечивает совместное использование сессий между несколькими серверами (балансировка нагрузки). Всегда используйте параметризованные запросы для предотвращения SQL-инъекций в ID сессий.

php
<?php
class DbSessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }

    public function open($path, $name): bool { return true; }
    public function close(): bool { return true; }

    public function read($id): string|false {
        $stmt = $this->pdo->prepare(
            'SELECT data FROM sessions WHERE id = ? AND expires > ?'
        );
        $stmt->execute([$id, time()]);
        return $stmt->fetchColumn() ?: '';
    }

    public function write($id, $data): bool {
        $exp = time() + (int)ini_get('session.gc_maxlifetime');
        return $this->pdo->prepare(
            'REPLACE INTO sessions (id, data, expires) VALUES (?, ?, ?)'
        )->execute([$id, $data, $exp]);
    }

    public function destroy($id): bool {
        return $this->pdo->prepare('DELETE FROM sessions WHERE id = ?')
            ->execute([$id]);
    }

    public function gc($max): int|false {
        return $this->pdo->prepare('DELETE FROM sessions WHERE expires < ?')
            ->execute([time()]);
    }
}

session_set_save_handler(new DbSessionHandler($pdo), true);
session_start();

Управление cookies

Используйте форму массива опций setcookie (PHP 7.3+) для ясности и установки SameSite. Secure cookies требуют HTTPS. HttpOnly предотвращает кражу cookies через XSS. SameSite=Lax блокирует кросс-сайтовые POST (достаточно для большинства CSRF-защиты); Strict блокирует все кросс-сайтовые запросы. Удаляйте cookies, устанавливая истечение в прошлом с тем же path/domain.

php
<?php
// Set a cookie with all security options
setcookie('preferences', json_encode(['theme' => 'dark']), [
    'expires' => time() + 86400 * 30,  // 30 days
    'path' => '/',
    'domain' => '.example.com',
    'secure' => true,                   // HTTPS only
    'httponly' => true,                 // No JavaScript access
    'samesite' => 'Lax'                 // CSRF protection
]);

// Read cookies
$theme = $_COOKIE['preferences'] ?? 'default';

// Delete a cookie (set expiration in the past)
setcookie('preferences', '', [
    'expires' => time() - 3600,
    'path' => '/',
]);

Flash-сообщения

Flash-сообщения хранят одноразовые уведомления в сессии, отображаемые после перенаправления (шаблон Post/Redirect/Get). Сообщение устанавливается до перенаправления и очищается после отображения. Это предотвращает предупреждения о повторной отправке и сохраняет UI чистым. Храните как массив для нескольких сообщений. Очищайте сразу после чтения для предотвращения устаревшего отображения.

php
<?php
// Set a flash message (one-time notification)
function flash(string $key, string $message): void {
    $_SESSION['_flash'][$key] = $message;
}

// Get and clear flash message
function getFlash(string $key): ?string {
    $msg = $_SESSION['_flash'][$key] ?? null;
    unset($_SESSION['_flash'][$key]);
    return $msg;
}

// Usage in controller
flash('success', 'Item saved!');
header('Location: /items');
exit;

// In view after redirect
if ($msg = getFlash('success')) {
    echo "<div class='alert'>$msg</div>";
}

JWT-аутентификация

JWT обеспечивает stateless-аутентификацию: сервер не хранит данные сессий, что идеально для API и микросервисов. Токен содержит claims (user ID, роль, истечение), подписанные секретом. Компромиссы: токены нельзя легко отозвать (используйте короткий срок + refresh-токены), и они увеличивают размер запроса. Используйте HttpOnly cookies для предотвращения кражи токена через XSS.

php
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Generate token on login
$payload = [
    'user_id' => 123,
    'role' => 'admin',
    'iat' => time(),           // Issued at
    'exp' => time() + 3600,    // Expires in 1 hour
];
$token = JWT::encode($payload, $secretKey, 'HS256');

// Send to client (cookie or Authorization header)
setcookie('auth_token', $token, [
    'expires' => time() + 3600,
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax',
]);

// Verify on each request
try {
    $decoded = JWT::decode(
        $_COOKIE['auth_token'],
        new Key($secretKey, 'HS256')
    );
    $userId = $decoded->user_id;
} catch (Exception $e) {
    http_response_code(401);
    exit('Unauthorized');
}
21

Углублённый REST API

Роутинг и обработка запросов

REST API отображают HTTP-методы на CRUD-операции: GET (чтение), POST (создание), PUT/PATCH (обновление), DELETE (удаление). Разбирайте URL-путь для идентификации ресурсов. Читайте тело запроса из php://input для POST/PUT. Всегда возвращайте соответствующие HTTP-коды состояния (200, 201, 400, 404, 500) и JSON-ответы с заголовком Content-Type.

php
<?php
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));

switch (true) {
    case $method === 'GET' && $segments[0] === 'users':
        if (isset($segments[1])) getUser((int)$segments[1]);
        else listUsers();
        break;

    case $method === 'POST' && $segments[0] === 'users':
        $data = json_decode(file_get_contents('php://input'), true);
        createUser($data);
        break;

    case $method === 'PUT' && $segments[0] === 'users' && isset($segments[1]):
        $data = json_decode(file_get_contents('php://input'), true);
        updateUser((int)$segments[1], $data);
        break;

    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not Found']);
}

Ответы и коды состояния

Всегда устанавливайте Content-Type: application/json для API-ответов. Используйте корректные коды состояния: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 422 (Unprocessable Entity), 429 (Too Many Requests), 500 (Server Error). Включайте детали ошибок для отладки, но никогда не раскрывайте трассировки стека в продакшене.

php
<?php
function jsonResponse($data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    exit;
}

// Success responses
jsonResponse(['data' => $users], 200);          // OK
jsonResponse(['data' => $user], 201);           // Created

// Error responses
jsonResponse(['error' => 'Validation failed'], 400);
jsonResponse(['error' => 'Unauthorized'], 401);
jsonResponse(['error' => 'Forbidden'], 403);
jsonResponse(['error' => 'Not Found'], 404);
jsonResponse(['error' => 'Server error'], 500);

Пагинация и фильтрация

Реализуйте пагинацию с LIMIT/OFFSET и возвращайте метаданные (total, текущая страница, всего страниц). Валидируйте и санитизируйте столбцы сортировки по белому списку для предотвращения SQL-инъекций. Ограничивайте per_page для предотвращения чрезмерных запросов. Используйте LIKE для поиска с подстановочными знаками. Возвращайте метаданные пагинации в объекте meta, отделённом от данных.

php
<?php
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;

// Whitelist sort columns (prevent SQL injection)
$sort = in_array($_GET['sort'] ?? '', ['name', 'email'])
    ? $_GET['sort'] : 'id';
$order = strtolower($_GET['order'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';

$sql = "SELECT * FROM users ORDER BY $sort $order LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$perPage, $offset]);
$users = $stmt->fetchAll();

// Total count for pagination metadata
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

jsonResponse([
    'data' => $users,
    'meta' => [
        'page' => $page,
        'per_page' => $perPage,
        'total' => $total,
        'total_pages' => ceil($total / $perPage),
    ],
]);

Rate limiting

Rate limiting предотвращает злоупотребление API. Используйте алгоритмы фиксированного окна (проще) или скользящего окна (точнее). Храните счётчики в Redis для распределённых систем. Возвращайте заголовки X-RateLimit (Limit, Remaining, Reset), чтобы клиенты могли саморегулироваться. HTTP 429 с Retry-After сообщает клиентам, когда повторять. Для продакшена используйте Redis или специализированный rate limiter.

php
<?php
function checkRateLimit(int $userId, int $max = 100, int $window = 3600): bool
{
    $file = sys_get_temp_dir() . "/rate_{$userId}_" . floor(time() / $window);
    $count = file_exists($file) ? (int)file_get_contents($file) : 0;

    if ($count >= $max) {
        $reset = (floor(time() / $window) + 1) * $window;
        header("X-RateLimit-Limit: $max");
        header("X-RateLimit-Remaining: 0");
        header("Retry-After: " . ($reset - time()));
        http_response_code(429);
        echo json_encode(['error' => 'Rate limit exceeded']);
        return false;
    }

    file_put_contents($file, $count + 1);
    header("X-RateLimit-Remaining: " . ($max - $count - 1));
    return true;
}

if (!checkRateLimit($userId)) exit;

Версионирование API

Стратегии версионирования API: префикс URL (/v1/) наиболее явный и cache-friendly; заголовок Accept RESTful, но сложнее в тестировании. Документируйте API через аннотации OpenAPI (Swagger). Генерируйте интерактивную документацию инструментами вроде swagger-php. Версонируйте с самого начала; ломающие изменения требуют новой версии. Депрекируйте старые версии заголовком Sunset.

php
<?php
// Versioning via URL prefix: /v1/users, /v2/users
$version = $segments[0] ?? 'v1';

// Versioning via Accept header
// Accept: application/vnd.myapp.v2+json
preg_match('/vnd\.myapp\.(v\d+)\+json/',
    $_SERVER['HTTP_ACCEPT'] ?? '', $m);
$version = $m[1] ?? 'v1';

// OpenAPI/Swagger documentation
/**
 * @OA\Get(path="/api/users",
 *   @OA\Response(response=200, description="List users")
 * )
 */
22

Углублённая безопасность (XSS/CSRF)

Предотвращение XSS

XSS (Cross-Site Scripting) внедряет вредоносные скрипты в веб-страницы. Предотвращайте кодированием вывода в зависимости от контекста: htmlspecialchars для HTML, json_encode для JavaScript, urlencode для URL. ENT_QUOTES экранирует и одинарные, и двойные кавычки. Content-Security-Policy (CSP) добавляет глубоко эшелонированную защиту, ограничивая источники скриптов. Никогда не доверяйте пользовательскому вводу.

php
<?php
// Output encoding (prevents XSS)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// JavaScript string (use json_encode)
echo '<script>var name = ' . json_encode($name) . ';</script>';

// URL parameter
echo 'redirect=' . urlencode($url);

// Content-Security-Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Disable inline scripts
header("X-XSS-Protection: 1; mode=block");

Защита CSRF

CSRF (Cross-Site Request Forgery) обманом заставляет пользователей отправлять нежелательные действия. Предотвращайте anti-CSRF токенами: генерируйте случайный токен для каждой сессии, включайте его в формы как скрытое поле и проверяйте на POST/PUT/DELETE. Используйте hash_equals для сравнения, защищённого от timing-атак. Для AJAX отправляйте токен в кастомном заголовке. SameSite=Strict cookies обеспечивают дополнительную защиту.

php
<?php
// Generate CSRF token
function csrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// Verify CSRF token (timing-safe)
function verifyCsrf(string $token): bool {
    return !empty($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

// In the form
echo '<input type="hidden" name="csrf_token" value="' . csrfToken() . '">';

// On POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
}

Предотвращение SQL-инъекций

SQL-инъекции позволяют атакующим выполнять произвольный SQL. Всегда используйте подготовленные запросы с параметризацией: БД разделяет SQL-логику и данные, делая инъекции невозможными. Никогда не конкатенируйте пользовательский ввод в SQL-строки. Для динамических запросов (конструкции IN, ORDER BY) стройте структуру SQL с плейсхолдерами и передавайте значения как параметры.

php
<?php
// BAD: string concatenation (SQL injection vulnerable)
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
// Attack: ?name=' OR '1'='1

// GOOD: prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_GET['name'], 'active']);
$users = $stmt->fetchAll();

// Named parameters
$stmt = $pdo->prepare(
    'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([':name' => $name, ':email' => $email]);

// Dynamic IN clause (still safe)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

Хэширование паролей

Никогда не храните пароли в открытом виде. password_hash использует bcrypt (или Argon2 при наличии) с автоматической генерацией соли. Хэш включает алгоритм, стоимость и соль, поэтому password_verify может проверять любой формат. Используйте password_needs_rehash для обновления хэшей при увеличении фактора стоимости или смене алгоритма. Argon2 рекомендуется для новых приложений.

php
<?php
// Hash a password (uses bcrypt by default, auto-salts)
$hash = password_hash('myPassword123', PASSWORD_DEFAULT);
// $2y$10$... (bcrypt with cost 10)

// Verify a password
if (password_verify($inputPassword, $storedHash)) {
    // Check if rehash is needed (algorithm upgrade)
    if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
        $newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
        // Update stored hash in database
    }
    // Log user in
}

// Argon2 (PHP 7.2+, requires libsodium)
$hash = password_hash('password', PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64 MB
    'time_cost'   => 4,      // iterations
    'threads'     => 2,
]);

Валидация ввода

Всегда валидируйте ввод на сервере (клиентская валидация только для UX). Используйте filter_input с FILTER_VALIDATE_* для проверки типов и FILTER_SANITIZE_* для очистки. Для кастомных правил используйте regex или специализированные библиотеки валидации (Respect/Validation, Symfony Validator). Используйте подход белого списка: принимайте только известные поля, отклоняйте всё остальное.

php
<?php
// Validate email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
    $errors[] = 'Invalid email';
}

// Validate integer with range
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150]
]);

// Validate URL
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);

// Custom validation with regex
function validateUsername(string $u): ?string {
    if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $u)) {
        return 'Username must be 3-20 alphanumeric chars';
    }
    return null;
}

// Whitelist approach for arrays
$allowed = ['name', 'email', 'age'];
$input = array_intersect_key($_POST, array_flip($allowed));
23

Углублённые пространства имён и автозагрузка

Объявление пространств имён

Пространства имён предотвращают коллизии имён классов и иерархически организуют код. Объявление namespace должно быть первым оператором. use импортирует классы, функции и константы. Алиасы (as) разрешают конфликты. Пространства имён PHP используют обратные слэши. Стандарт PSR-4 отображает разделители пространств имён на разделители каталогов: MyApp\Services\UserService -> src/Services/UserService.php.

php
<?php
// File: src/Services/UserService.php
namespace MyApp\Services;

use MyApp\Models\User;
use MyApp\Repositories\UserRepository;
use MyApp\Exceptions\NotFoundException;

class UserService
{
    public function __construct(
        private UserRepository $repo
    ) {}

    public function find(int $id): User
    {
        $user = $this->repo->findById($id);
        if (!$user) {
            throw new NotFoundException("User {$id} not found");
        }
        return $user;
    }
}

PSR-4 автозагрузка

PSR-4 — стандартная спецификация автозагрузки: префикс пространства имён отображается на базовый каталог, а каждый разделитель пространства имён становится разделителем каталогов. Composer генерирует автозагрузчик, который автоматически разрешает имена классов в пути файлов. Запускайте composer dump-autoload после добавления новых классов. Автозагрузчик загружает классы только при первом обращении (ленивая загрузка).

php
// composer.json
{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src/",
            "Tests\\": "tests/"
        }
    }
}

// After composer install:
require __DIR__ . '/vendor/autoload.php';

// Now all classes auto-load:
use MyApp\Services\UserService;
use MyApp\Controllers\UserController;

// PSR-4 rules:
// MyApp\User          -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
// MyApp\Models\User\Profile -> src/Models/User/Profile.php

Кастомный автозагрузчик

spl_autoload_register добавляет функцию в стек автозагрузчиков. Когда класс указан, но не загружен, PHP вызывает каждый зарегистрированный автозагрузчик по порядку, пока один не загрузит класс. Может сосуществовать несколько автозагрузчиков (например, один для PSR-4, один для legacy-классов). Всегда проверяйте, существует ли файл, перед require во избежание ошибок. Composer использует этот механизм внутри.

php
<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'MyApp\\';
    $baseDir = __DIR__ . '/src/';

    // Check if class uses our namespace prefix
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;  // Not our class
    }

    // Get relative class name
    $relativeClass = substr($class, $len);

    // Replace namespace separators with directory separators
    $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

    if (file_exists($file)) {
        require $file;
    }
});

// Multiple autoloaders can be registered (stack-based)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/legacy/' . $class . '.php';
    if (file_exists($path)) require $path;
});

Classmap и files автозагрузка

classmap-автозагрузка сканирует каталоги во время dump-autoload и строит массив, отображающий имена классов на пути файлов. Это быстрее PSR-4 (один поиск в массиве вместо проверок файловой системы) и рекомендуется для продакшена. files автозагружает конкретные файлы при каждом запросе — полезно для вспомогательных функций и констант, которые нельзя автозагрузить как классы. Используйте --optimize для продакшен-classmap.

php
{
    "autoload": {
        "psr-4": { "MyApp\\": "src/" },
        "classmap": ["src/Legacy/", "lib/OldClasses.php"],
        "files": ["src/helpers.php", "src/constants.php"]
    }
}

// classmap: scans directories and builds class-to-file map
// Faster than PSR-4 for production (no filesystem checks)

// files: always-loaded files (for functions and constants)
// src/helpers.php:
<?php
function dd($var) { var_dump($var); die; }
function env(string $key, $default = null) { /* ... */ }

Разрешение пространств имён

В коде с пространствами имён неквалифицированные имена классов разрешаются сначала через импорты, затем через текущее пространство имён. Встроенные классы (DateTime, PDO, Exception) живут в глобальном пространстве имён; ссылайтесь на них ведущим обратным слэшем или импортируйте. Функции и константы возвращаются к глобальному пространству имён, если не найдены локально. Используйте FQCN (ведущий обратный слэш) для абсолютных ссылок.

php
<?php
namespace MyApp\Services;

use MyApp\Models\User;

class UserService
{
    public function create(): User
    {
        // User resolves to MyApp\Models\User (imported)
        return new User();
    }

    public function find(): \MyApp\Models\User
    {
        // Fully Qualified Class Name (leading backslash)
        return new \MyApp\Models\User();
    }

    public function date(): \DateTime
    {
        // Global classes need backslash or import
        return new \DateTime();
    }
}
24

Генераторы и yield

Базовый генератор

Генераторы производят значения лениво через yield, по одному за раз, без построения всей коллекции в памяти. Это эффективно по памяти для больших или бесконечных последовательностей. Функция возвращает объект Generator, реализующий Iterator. Каждый yield приостанавливает выполнение, возобновляется на следующей итерации. Используйте генераторы для чтения больших файлов, курсоров БД и вычисляемых последовательностей.

php
<?php
function rangeGen(int $start, int $end, int $step = 1): Generator
{
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

// Iterate without loading all values into memory
foreach (rangeGen(1, 5) as $value) {
    echo $value . ' ';  // 1 2 3 4 5
}

// Memory efficient for large sequences
foreach (rangeGen(1, 1000000) as $value) {
    if ($value % 100000 === 0) echo "Reached {$value}\n";
}

Возврат пар ключ-значение

Генераторы могут возвращать пары ключ-значение через синтаксис yield key => value, как ассоциативные массивы. Это сохраняет ключи через преобразования. Для фильтрации значений просто не возвращайте их. Generator сохраняет свою позицию в итерации, поэтому можно построить конвейерную обработку, где каждый генератор преобразует или фильтрует поток.

php
<?php
function mapGen(array $data): Generator
{
    foreach ($data as $key => $value) {
        yield $key => strtoupper($value);
    }
}

foreach (mapGen(['a' => 'hello', 'b' => 'world']) as $key => $value) {
    echo "{$key} => {$value}\n";
}
// a => HELLO
// b => WORLD

// Filter by not yielding unwanted values
function filterGen(array $data): Generator
{
    foreach ($data as $value) {
        if ($value > 0) yield $value;
    }
}

Отправка значений в генераторы

Метод send() передаёт значение в генератор, становясь результатом выражения yield. Это обеспечивает двунаправленную связь, полезную для корутин и конечных автоматов. current() запускает генератор. getReturn() получает возвращаемое значение после завершения генератора. Блок finally выполняется при уничтожении генератора, обеспечивая очистку ресурсов.

php
<?php
function accumulator(): Generator
{
    $total = 0;
    while (true) {
        $value = yield $total;
        if ($value === null) break;
        $total += $value;
    }
    return $total;
}

$gen = accumulator();
$gen->current();  // Start: 0
$gen->send(10);   // Returns 10
$gen->send(20);   // Returns 30
$gen->send(5);    // Returns 35
$gen->send(null); // Triggers return
echo $gen->getReturn();  // 35

yield from (делегирование)

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

php
<?php
function innerGen(): Generator
{
    yield 1;
    yield 2;
    return 'inner done';
}

function outerGen(): Generator
{
    yield 0;
    $result = yield from innerGen();
    echo "Inner returned: {$result}\n";
    yield 3;
}

foreach (outerGen() as $value) {
    echo $value . ' ';  // 0 1 2 3
}
// Inner returned: inner done

// Yield from arrays
function mixedGen(): Generator {
    yield from [10, 20, 30];
    yield from new ArrayObject([40, 50]);
}

Практические случаи применения

Генераторы преуспевают в обработке больших или бесконечных потоков данных: построчное чтение файлов, итерация курсоров БД, постраничное получение API и математические последовательности. Шаблон take() ограничивает бесконечный генератор. Генераторы хорошо компонуются: передавайте данные через несколько генераторов для фильтрации, маппинга и редукции. Память остаётся постоянной независимо от размера данных.

php
<?php
// 1. Read large files line by line (memory efficient)
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (!feof($handle)) {
        $line = fgets($handle);
        if ($line !== false) yield rtrim($line);
    }
    fclose($handle);
}

foreach (readLines('large.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo "{$line}\n";
    }
}

// 2. Infinite sequence with take()
function naturals(): Generator {
    $n = 1;
    while (true) yield $n++;
}

function take(Generator $gen, int $n): Generator {
    for ($i = 0; $i < $n; $i++) {
        yield $gen->current();
        $gen->next();
    }
}

foreach (take(naturals(), 5) as $num) {
    echo $num . ' ';  // 1 2 3 4 5
}
25

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

Предотвращение SQL-инъекций

SQL-инъекции возникают, когда пользовательский ввод конкатенируется в SQL. Всегда используйте подготовленные запросы с параметризацией. И PDO, и MySQLi их поддерживают. Никогда не доверяйте пользовательскому вводу. Валидируйте и санитизируйте все внешние данные.

php
// BAD: vulnerable
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
// GOOD: prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();

Предотвращение XSS

XSS (Cross-Site Scripting) внедряет вредоносные скрипты. htmlspecialchars преобразует специальные символы в HTML-сущности. ENT_QUOTES экранирует и одинарные, и двойные кавычки. Всегда экранируйте при выводе пользовательских данных в HTML. Используйте заголовки Content-Security-Policy для глубоко эшелонированной защиты.

php
// BAD: output without escaping
echo $_GET['name'];
// GOOD: escape output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For HTML attributes
echo 'value="' . htmlspecialchars($value, ENT_QUOTES) . '"';

Хэширование паролей

password_hash использует bcrypt (или argon2) с автоматической генерацией соли. Никогда не используйте md5 или sha1 для паролей. password_verify проверяет пароль против хэша. password_needs_rehash позволяет обновлять алгоритмы хэширования. Соль встроена в строку хэша.

php
// Hash a password
$hash = password_hash('mypassword', PASSWORD_DEFAULT);
// Verify
if (password_verify('mypassword', $hash)) {
    echo 'Valid password';
}
// Check if needs rehash
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash('mypassword', PASSWORD_DEFAULT);
}

Защита CSRF

CSRF (Cross-Site Request Forgery) обманом заставляет пользователей выполнять нежелательные действия. Генерируйте случайный токен для каждой сессии. Включайте его в формы как скрытое поле. Проверяйте на POST через hash_equals (сравнение, защищённое от timing-атак). SameSite cookies обеспечивают дополнительную защиту.

php
session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
// In form
echo '<input type="hidden" name="token" value="' . $_SESSION['token'] . '">';
// Verify
if (!hash_equals($_SESSION['token'], $_POST['token'] ?? '')) {
    die('CSRF token mismatch');
}

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

cookie_httponly предотвращает доступ через JavaScript. cookie_secure обеспечивает только HTTPS. samesite=Strict предотвращает CSRF. use_strict_mode отклоняет неинициализированные ID сессий. session_regenerate_id предотвращает фиксацию сессии. Всегда регенерируйте после изменения привилегий.

php
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);  // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate ID after login
session_regenerate_id(true);

Was this helpful?