PHP Core Knowledge Base
Подробное руководство по PHP для backend-разработчиков
1. Типы данных PHP
Основные типы (Basic Types)
| Тип | Описание | Примеры |
|---|---|---|
| int | Целое число (64-bit на современных системах) | 42, -100, 0x1A |
| float | Число с плавающей точкой (double) | 3.14, 1.2e-3 |
| string | Строка символов | "Hello", 'World' |
| bool | true или false | true, false |
| array | Коллекция элементов с ключами | [1, 2, 3], ['a' => 1] |
| object | Экземпляр класса | new User() |
| null | Отсутствие значения | null |
| resource | Ссылка на внешний ресурс | файлы, БД соединения |
Type Juggling и Type Casting
(тип)$value. Различие в инициаторе и контроле: juggling непредсказуем в сложных выражениях, casting даёт полный контроль.
// Строка + число — строка превращается в число
echo "5" + 3; // 8 (string "5" → int 5)
// Ведущие цифры строки превращаются в число, остальное отбрасывается
echo "10 apples" + 5; // 15
// Нечисловая строка превращается в 0
echo "hello" + 5; // 5 ("hello" → 0)
// Оператор конкатенации (точка) — наоборот, число становится строкой
echo 5 . " apples"; // "5 apples"
// === PHP 8.0+ (актуальное поведение) ===
var_dump("10" == 10); // true (строка ЧИСЛОВАЯ → сравнение как числа)
var_dump("10" === 10); // false (строгое: типы разные)
var_dump("" == 0); // false ← было true до PHP 8.0!
var_dump("abc" == 0); // false ← было true до PHP 8.0!
var_dump("1abc" == 1); // false ← было true до PHP 8.0!
var_dump("0" == 0); // true (строка числовая, значение 0)
var_dump("100" == "1e2"); // true (обе строки числовые: 100 == 100)
- До 8.0: сравнение строки с числом → строка приводится к числу. Нечисловая строка →
0. Поэтому"abc" == 0былоtrue— источник классических багов в безопасности. - С 8.0+: если строка нечисловая — число приводится к строке, сравниваются две строки. Если строка числовая (
"10","1e2") — по-прежнему сравниваются как числа. - Запоминать одно правило: числовая строка ↔ число — как числа; нечисловая ↔ число — как строки.
=== и !== — они проверяют и тип, и значение. Это особенно критично при работе с данными из форм, JSON, БД, где тип может приходить неожиданным.
(int), (float), (bool) — это встроенный оператор языка, не функция. Общая форма: (тип) выражение. Скобки обязательны: они отделяют имя типа от остального кода. PHP вычисляет выражение справа и приводит результат к указанному типу. В отличие от функции, оператор не имеет вызывающих скобок вокруг аргумента: (int)$x работает, (int)($x) — тоже, но скобки вокруг x — это просто группировка выражения, не часть синтаксиса оператора. Доступные операторы: (int)/(integer), (float)/(double), (string), (bool)/(boolean), (array), (object), (unset) (deprecated с PHP 7.2).
// (int) — отбрасывает дробную часть, не округляет
$int = (int)"42.99"; // 42
$int = (int)"123hello"; // 123
$int = (int)true; // 1
// (float) — целое становится дробью
$float = (float)"3.14"; // 3.14
$float = (float)"5"; // 5.0
// (bool) — falsy значения: 0, "", "0", [], null
$bool = (bool)0; // false
$bool = (bool)""; // false
$bool = (bool)"0"; // false (важная ловушка!)
$bool = (bool)[]; // false
$bool = (bool)"hello"; // true
$bool = (bool)"false"; // true (любая непустая строка кроме "0")
// (string)
$string = (string)123; // "123"
$string = (string)true; // "1"
$string = (string)false; // "" (пустая строка, не "false"!)
$string = (string)null; // ""
// (array)
$array = (array)42; // [42] — скаляр становится массивом с одним элементом
$array = (array)$object; // свойства объекта → элементы массива
// (object)
$obj = (object)["a" => 1, "b" => 2];
// stdClass с $obj->a = 1, $obj->b = 2
var_dump($obj) построчноvar_dump($obj);
// Вывод:
// object(stdClass)#1 (2) {
// ["a"]=> int(1)
// ["b"]=> int(2)
// }
// Что значит каждая часть:
//
// object — тип значения: объект (не array, не string)
// (stdClass) — имя класса экземпляра
// #1 — внутренний ID объекта в этом дампе (для отслеживания
// рекурсивных ссылок: если объект появится дважды,
// во второй раз увидишь "#1" повторно — это та же сущность)
// (2) — количество свойств у объекта
// ["a"]=> int(1) — свойство "a" типа int со значением 1
// ["b"]=> int(2) — свойство "b" типа int со значением 2
stdClass?
stdClass — это встроенный пустой класс PHP. Не имеет методов, свойств, констант. Используется как «универсальный контейнер» когда нужен объект без определения своего класса.
PHP автоматически создаёт stdClass в трёх случаях:
new stdClass()— явно(object)$array— приведение массива к объектуjson_decode($json)без второго аргументаtrue— JSON-объекты становятсяstdClass
| Аспект | Ассоциативный массив | Объект |
|---|---|---|
| Что это | Упорядоченная коллекция пар «ключ → значение» | Экземпляр класса с свойствами (и методами) |
| Доступ к данным | $person["name"] | $person->name |
| Создание | ['name' => 'Иван', 'age' => 30] | (object)['name'=>'Иван'] или new stdClass() |
| Добавить поле | $arr['email'] = 'a@b.ru' | $obj->email = 'a@b.ru' |
| Методы / поведение | Нет (только функции работают с массивом снаружи) | Можно добавить через свой класс |
| Передача в функцию | Копирование (CoW под капотом) | По ссылке (одна сущность в памяти) |
| Проверка поля | isset($arr['name']) / array_key_exists | isset($obj->name) / property_exists |
var_dump | array(2) { ["name"]=> ... } | object(stdClass)#1 (2) { ["name"]=> ... } |
| Сериализация в JSON | json_encode — JSON-объект | json_encode — тоже JSON-объект |
| Тип в type-hint | array | object / конкретный класс |
| Производительность | Чуть быстрее | Чуть медленнее, но разница не критична |
- Ассоциативный массив — конфиги, простые DTO без поведения, передача данных «как есть», быстрые трансформации (
array_map,array_filter,ksort). - Объект (свой класс) — сущности с поведением (User с
register(), Order сcalculateTotal()), важна типизация (type-hints, IDE-подсказки), наследование, инкапсуляция. stdClass— временный «контейнер» когда не хочется создавать класс: результатjson_decode, быстрый DTO для шаблона (удобнее$item->nameчем$item['name']). В production-коде лучше явный класс — больше контроля.
(array)$object ключами становятся имена свойств, значениями — их значения. Но видимость (public/protected/private) меняет формат ключа: PHP добавляет служебные null-байты для скрытия инкапсуляции.
// 1. Public — обычные ключи
$obj = new stdClass();
$obj->name = "Alice";
$obj->age = 30;
$array = (array)$obj;
// [ "name" => "Alice", "age" => 30 ]
// 2. Protected — ключ "\0*\0имя" (null-байт + звёздочка + null-байт)
class User {
protected $id = 123;
public $name = "Bob";
}
$array = (array)new User();
// [ "\0*\0id" => 123, "name" => "Bob" ]
// 3. Private — ключ "\0ИмяКласса\0имя" (null-байт + имя класса + null-байт)
class Person {
private $ssn = "123-45-6789";
public $city = "London";
}
$array = (array)new Person();
// [ "\0Person\0ssn" => "123-45-6789", "city" => "London" ]
// 4. Пустой объект → пустой массив
$array = (array)new stdClass(); // []
// 5. Что НЕ попадает:
// - методы объекта (только данные)
// - статические свойства (принадлежат классу, не экземпляру)
// - константы класса
+------------------+-------------------------+
| Видимость | Формат ключа в массиве |
+------------------+-------------------------+
| public | "имя" |
| protected | "\0*\0имя" |
| private | "\0ИмяКласса\0имя" |
+------------------+-------------------------+
// \0 — это NULL-байт (ASCII 0), непечатный символ.
// var_dump покажет его как \0, print_r — как пустоту.
// Обращение через $array["\0*\0id"] технически работает,
// но нарушает инкапсуляцию — не используйте в нормальном коде.
get_object_vars() (вернёт только видимые из текущей области свойства, без null-байтов). Прямое (array) подходит только для stdClass или внутри тех же классов.
// (string) на объект сработает только если в классе есть __toString()
class User {
public string $name = 'unknown';
public function __toString(): string {
return $this->name;
}
}
$user = new User();
echo (string)$user; // "unknown"
echo "Hello, {$user}"; // "Hello, unknown" — тоже использует __toString
// Без __toString — Error: Object of class User could not be converted to string
+----------------+--------------------------+--------------------------+
| Характеристика | Type Juggling | Type Casting |
+----------------+--------------------------+--------------------------+
| Инициатор | PHP автоматически | Разработчик явно |
| Контроль | Низкий, скрытые правила | Полный |
| Безопасность | Маскирует ошибки | Видно что происходит |
| | "abc" + 2 → 2 | |
| Применимость | Простые скрипты, формы | Валидация, бизнес-логика |
| Рекомендация | Избегать в продакшене | Использовать всегда |
+----------------+--------------------------+--------------------------+
// 1. Числовая строка с "мусором" — PHP молча берёт цифры
$value = "12.34 руб.";
echo $value + 10; // 22.34 — работает, но непредсказуемо для других строк
// 2. Строка "0" — единственная непустая falsy-строка
if ("0") { /* НЕ выполнится */ }
if ("false") { /* выполнится (любая другая строка — truthy) */ }
// 3. Логические операции с небулевыми значениями
var_dump("a" && "b"); // true (обе truthy)
var_dump("0" && "b"); // false ("0" → false)
// 4. (int) от float с погрешностью
echo (int)((0.1 + 0.7) * 10); // 7, а не 8! (0.1 + 0.7 = 0.7999...)
// 5. Массив в bool — пустой массив false, любой непустой true
var_dump((bool)[0]); // true (массив непуст, хотя элемент 0)
// 6. NULL в сравнениях
var_dump(null == false); // true
var_dump(null === false); // false
var_dump(null == 0); // true
var_dump(null == ""); // true
// Опасно — полагаемся на juggling
function calculate($amount, $quantity) {
return $amount * $quantity; // "abc" * "2" = 0
}
// Безопасно — явная проверка типов
function calculate($amount, $quantity): float {
if (!is_numeric($amount) || !is_numeric($quantity)) {
throw new InvalidArgumentException('Numeric expected');
}
return (float)$amount * (int)$quantity;
}
// Безопасные парсеры строк → числа
$int = intval($str, 10); // явное основание 10 — защита от "077" → 7 (octal)
$float = floatval($str);
$value = filter_var($str, FILTER_VALIDATE_INT); // вернёт false если не int
$value = filter_var($str, FILTER_VALIDATE_FLOAT); // вернёт false если не float
1. Используйте
=== / !== вместо == / != — всегда.2. Приводите типы явно через
(int), (float), не надейтесь на juggling.3. Проверяйте тип перед операцией:
is_numeric(), is_string(), is_array().4. Для парсинга чисел из строк используйте
filter_var($v, FILTER_VALIDATE_INT).5. Включите
declare(strict_types=1); в начале каждого файла (см. следующую подсекцию).6. Помните про
__toString(): (string)$object упадёт, если метод не определён.
Операторы сравнения и присваивания (=, ==, ===, !=, !==)
=) вообще не сравнение, остальные различаются в том, приводит ли PHP типы перед сравнением. Правильный выбор между ними определяет, сработает ли ваше условие так, как вы думаете.
= — присваивание (assignment)// Действие, не сравнение: кладёт значение справа в переменную слева
$x = 5; // $x теперь равно 5
// Возвращаемое значение присваивания = присвоенное значение
if ($x = 10) { // ОПАСНО! Присваивает 10, не сравнивает.
// Условие всегда true (10 — truthy).
// Большинство IDE/линтеров подсветят это как warning.
}
= внутри условий допустимо только осознанно (например, для inline-присваивания: while (($row = $stmt->fetch()) !== false) { ... }). Обычная ошибка — написать = вместо ==/===.
== — нестрогое равенство// Сравнивает значения после приведения типов (type juggling)
var_dump(5 == "5"); // true (строка "5" → число 5)
var_dump(5 == "6"); // false ("6" → 6, 5 != 6)
var_dump(0 == false); // true (false → 0)
var_dump("" == 0); // true (пустая строка → 0)
var_dump("abc" == 0); // до PHP 8.0: true; с 8.0+: false
var_dump(42 == "42 apples"); // true ("42 apples" → 42)
=== — строгое равенство (рекомендуется по умолчанию)// Сравнивает И значение, И тип — без приведения
var_dump(5 === "5"); // false (int vs string)
var_dump(5 === 5); // true
var_dump(0 === false); // false (int vs bool)
var_dump(null === null); // true
var_dump(null === false); // false (null vs bool)
!= и <> — нестрогое неравенство// Противоположность == — true, если значения НЕ равны после приведения
var_dump(5 != "5"); // false (равны после juggling)
var_dump(5 != "6"); // true
var_dump(0 != false); // false (равны)
var_dump(0 != "abc"); // до PHP 8.0: false; с 8.0+: true
// <> — синоним != (тот же оператор)
var_dump(5 <> "5"); // false
!== — строгое неравенство// Противоположность === — true, если значения ИЛИ типы различаются
var_dump(5 !== "5"); // true (разные типы)
var_dump(5 !== 5); // false
var_dump(0 !== false); // true (int vs bool)
var_dump(null !== null); // false
+-----------+---------------------+------------------+--------------------+
| Оператор | Название | Приводит типы? | Пример: 5 op "5" |
+-----------+---------------------+------------------+--------------------+
| = | Присваивание | N/A (не сравн.) | присвоит "5" |
| == | Нестрогое равенство | да (juggling) | true |
| === | Строгое равенство | нет | false |
| != / <> | Нестрогое неравен. | да (juggling) | false |
| !== | Строгое неравенство | нет | true |
+-----------+---------------------+------------------+--------------------+
// 1. Пропущенный знак в условии
if ($x = 5) { ... } // присваивание (всегда true)
if ($x == 5) { ... } // нестрогое сравнение
if ($x === 5) { ... } // строгое (правильно)
// 2. Сравнение со строкой через ==
if ($str == 0) { ... }
// сработает для "", "abc", "0" и null — почти наверняка не то, что нужно
if ($str === "0") { ... }
// только для конкретной строки "0"
// 3. Возврат функции false vs 0 vs ""
$result = strpos("hello", "h"); // 0 (позиция первого символа)
if (!$result) { ... } // СРАБОТАЕТ: 0 → false. Баг!
if ($result === false) { // правильно — strpos вернёт false если не найдено
}
// 4. in_array без strict mode
in_array(0, ["a", "b", "c"]); // true в PHP < 8.0 ("a" → 0)
in_array(0, ["a", "b", "c"], true); // false (строгий режим)
// 5. NULL и пустые значения
var_dump(null == false); // true
var_dump(null == 0); // true
var_dump(null == ""); // true
var_dump(null == "0"); // false (важно: "0" != null)
= — положить (присвоить).== — «похожи» после приведения типов.=== — абсолютно идентичны (тип + значение).!= — «не похожи» после приведения.!== — не идентичны (либо тип, либо значение, либо оба).По умолчанию используйте
=== и !==. Нестрогое сравнение оправдано только когда осознанно нужно приведение типов (например, сравнение пользовательского ввода с числом) — и даже тогда лучше явно: (int)$input === 5.
Compound операторы (+=, -=, *=, /=, %=, .=)
// Каждая пара эквивалентна:
// += (сложение / конкатенация чисел)
$x = 5;
$x += 3; // эквивалент: $x = $x + 3 → $x теперь 8
// -= (вычитание)
$x -= 2; // $x = $x - 2 → 6
// *= (умножение)
$x *= 10; // $x = $x * 10 → 60
// /= (деление)
$x /= 4; // $x = $x / 4 → 15
// %= (остаток от деления)
$x %= 7; // $x = $x % 7 → 1 (15 % 7 = 1)
// **= (возведение в степень, PHP 5.6+)
$y = 2;
$y **= 3; // $y = $y ** 3 → 8
// .= (конкатенация строк)
$s = "Hello";
$s .= ", World"; // $s = $s . ", World" → "Hello, World"
// Битовые: &=, |=, ^=, <<=, >>= — используются реже, в бизнес-логике почти не встречаются
$flags = 0b1010;
$flags |= 0b0001; // побитовое ИЛИ → 0b1011
$flags &= 0b1100; // побитовое И → 0b1000
// 1. Аккумуляция суммы / счётчика
$total = 0;
foreach ($orders as $order) {
$total += $order['amount']; // прирост total на сумму заказа
}
// 2. Внутри array_reduce — изменение полей аккумулятора
array_reduce($items, function($carry, $item) {
$carry['sum'] += $item;
$carry['count']++; // ++ — это +=1 для целых
return $carry;
}, ['sum' => 0, 'count' => 0]);
// 3. Построение строки по частям
$html = '';
foreach ($rows as $row) {
$html .= "<tr><td>{$row['name']}</td></tr>";
}
// 4. Декремент через -=1 (то же что --)
$retries = 3;
while ($retries > 0) {
// ... попытка ...
$retries -= 1; // эквивалент: $retries-- или --$retries
}
+----------+---------------------------+-----------------------+
| Оператор | Эквивалент | Применение |
+----------+---------------------------+-----------------------+
| += | $a = $a + $b | сумма, счётчик |
| -= | $a = $a - $b | вычитание, декремент |
| *= | $a = $a * $b | умножение |
| /= | $a = $a / $b | деление |
| %= | $a = $a % $b | остаток |
| **= | $a = $a ** $b | степень (PHP 5.6+) |
| .= | $a = $a . $b | конкатенация строк |
| &= | $a = $a & $b | битовое И |
| |= | $a = $a | $b | битовое ИЛИ |
| ^= | $a = $a ^ $b | битовое XOR |
| ??= | $a = $a ?? $b | null-coalescing (PHP 7.4+) |
+----------+---------------------------+-----------------------+
??= (PHP 7.4+) — присваивает значение только если переменная сейчас null или не определена. Удобно для дефолтов: $config['timeout'] ??= 30; положит 30, только если ключ ещё не задан. Полезно при работе с массивами настроек.
Логические операторы (&&, ||, !, and, or, xor)
if, while, тернарных выражениях, match. PHP имеет два набора: символьные (&&, ||) и словесные (and, or). Они почти эквивалентны, но разный приоритет — это критично.
| Оператор | Что делает | Пример | Приоритет |
|---|---|---|---|
! (NOT) | Инвертирование | !is_numeric($x) → true если НЕ число | Высокий |
&& (AND) | true если ОБА | $a > 0 && $a < 10 | Высокий |
|| (OR) | true если ХОТЯ БЫ ОДИН | $role === 'admin' || $isOwner | Высокий |
and | То же что &&, но НИЖЕ приоритет | $x = $a and $b → ($x = $a) and $b | Низкий |
or | То же что ||, но НИЖЕ приоритет | см. ловушку ниже | Низкий |
xor | true если РОВНО ОДИН | $a xor $b — взаимоисключающее ИЛИ | Низкий |
| A | B | A && B | A || B | A xor B |
|---|---|---|---|---|
| true | true | true | true | false |
| true | false | false | true | true |
| false | true | false | true | true |
| false | false | false | false | false |
// PHP вычисляет операнды СЛЕВА НАПРАВО и останавливается
// как только результат уже известен.
// && — если ЛЕВЫЙ false, правый НЕ вычисляется:
$user = null;
if ($user !== null && $user->isAdmin()) { ... }
// $user->isAdmin() НЕ вызовется на null — нет Error.
// || — если ЛЕВЫЙ true, правый НЕ вычисляется:
$result = $cached ?: expensive_lookup();
// expensive_lookup() не вызовется, если $cached истинно.
// Практическое применение: guard clauses
if (!is_numeric($amount) || !is_numeric($quantity)) {
throw new InvalidArgumentException('Numeric expected');
}
// Если первое условие true (amount не число) — второе даже не проверяется.
|| vs or// Эти две строки выглядят одинаково — но РАБОТАЮТ ПО-РАЗНОМУ:
$result = false || true;
// Скобки: $result = (false || true) → $result = true
$result = false or true;
// Скобки: ($result = false) or true → $result = FALSE !!!
// Потому что = имеет ВЫШЕ приоритет чем or, но НИЖЕ чем ||.
// ПРАВИЛО: в новом коде всегда используй && и ||.
// or / and / xor оставлены для совместимости — почти не применяются.
// 1. Несколько guard-условий перед основной логикой
public function register(User $user): void
{
if (!$user->isVerified() || $user->isBanned() || $user->age < 18) {
throw new RuntimeException('Cannot register');
}
// ... основная логика
}
// 2. Проверка любого из ролей (OR)
if ($user->hasRole('admin') || $user->hasRole('manager')) {
// доступ разрешён хотя бы одной из ролей
}
// 3. Range check (AND)
if ($age >= 18 && $age <= 65) {
// возраст в диапазоне
}
// 4. Сложные условия со скобками
if (($role === 'admin' && $user->active) || $user->id === $ownerId) {
// (active админ) ИЛИ (владелец) — скобки явно фиксируют логику
}
- Используй
&&и||— неand/or. Разница только в приоритете, и приоритет у них коварный. - Short-circuit — это не оптимизация, это инструмент. Активно применяют для безопасной проверки nullable объектов:
$user !== null && $user->isAdmin(). - В сложных условиях — скобки. Не полагайся на приоритет
&&выше||— это работает, но читателю надо вспоминать. Скобки делают код самодокументируемым.
Тернарный, Elvis, null coalescing (?:, ??, ?->)
?, но семантика разная. Часто путают — особенно тернарный vs ??.
условие ? a : b// Тернарный — сокращённый if/else в виде выражения (возвращает значение).
// Синтаксис: условие ? значение_если_true : значение_если_false
$age = 18;
$status = $age >= 18 ? 'adult' : 'minor';
// $status = 'adult'
// Эквивалентно:
// if ($age >= 18) { $status = 'adult'; } else { $status = 'minor'; }
// Вложенные — читаются плохо, лучше match:
$tier = $score > 90 ? 'gold' : ($score > 50 ? 'silver' : 'bronze');
?: — сокращённый тернарный// Если средняя часть тернарного — то же что условие, можно её опустить:
// Длинная форма:
$name = $input ? $input : 'Anonymous';
// Elvis (сокращённая):
$name = $input ?: 'Anonymous';
// Если $input истинно — вернёт $input, иначе 'Anonymous'.
// Работает по TRUTHY: '0', '', 0, null, [] — все вернут 'Anonymous'.
// Если нужно проверять именно null — используй ??.
?? (PHP 7+)// $a ?? $b — вернёт $a, если оно НЕ null (и определено),
// иначе $b.
// Отличие от Elvis: ?? проверяет ТОЛЬКО null/undefined,
// не учитывает '0', '', 0 как пустоту.
$page = $_GET['page'] ?? 1;
// Если $_GET['page'] = '0' → $page = '0' (НЕ 1!)
// Если $_GET['page'] не задан → $page = 1.
// Цепочки:
$value = $config['override'] ?? $config['default'] ?? 'fallback';
// первое непустое значение, по цепочке.
// ??= — присвоение если null (PHP 7.4+):
$config['timeout'] ??= 30;
// если ключа нет / null — положит 30. Иначе оставит как есть.
?-> (PHP 8+)// Цепочка обращений к свойствам/методам, безопасная для null.
// Если любая часть цепочки null — вся выражение возвращает null,
// БЕЗ Error и без NullPointerException.
// Без nullsafe — стандартная проверка:
$avatar = ($user !== null && $user->profile !== null)
? $user->profile->avatar
: null;
// С nullsafe — короче:
$avatar = $user?->profile?->avatar;
// Если $user null → весь результат null.
// Если $user->profile null → результат null.
// Иначе — значение avatar.
// Работает только на ЧТЕНИЕ. Нельзя писать через ?->:
// $user?->profile = $p; // SYNTAX ERROR
| Оператор | Версия | Что проверяет | Когда применять |
|---|---|---|---|
? : (тернарный) | любая | Truthy/falsy условие | Простой выбор из двух значений |
?: (Elvis) | 5.3+ | Truthy левого операнда | «Значение или дефолт» когда дефолт нужен для всех falsy |
?? | 7.0+ | Только null / undefined | «Значение или дефолт» когда 0/'' — валидные значения |
??= | 7.4+ | null / undefined | Лениво задать дефолт в массиве настроек |
?-> | 8.0+ | null в середине цепочки | Цепочка обращений к опциональным объектам |
?: и ???
?:срабатывает на любое falsy (0,'','0',null,[]).??срабатывает ТОЛЬКО наnullили undefined ключ.- Для дефолтов из
$_GET/ API → используй??(иначе?page=0заменится на дефолт). - Для дефолтов «когда строка непустая» →
?:ок.
Двоеточие : — все контексты использования
: встречается в PHP в 5 разных контекстах. Главный — return type declaration в функциях. Часто спрашивают «что значит : float» — точный ответ ниже.
function calculate(int $a, int $b): float
{
return $a * $b / 2;
}
// ↑
// двоеточие перед типом
// : float означает: "эта функция ОБЯЗАНА вернуть float
// (число с плавающей точкой)".
// Если функция вернёт другой тип, PHP попытается привести:
// return 42 → 42.0 (int → float)
// return "3.14" → 3.14 (numeric string → float)
// return "abc" → TypeError
// Со strict_types=1 автоматического приведения нет —
// возврат int вместо float тоже выбросит TypeError.
| Тип | Что означает | Версия |
|---|---|---|
: void | Функция НИЧЕГО не возвращает (нельзя return $x) | 7.1+ |
: int / : float / : string / : bool / : array | Скалярные / массив | 7.0+ |
: ?int | Nullable: int или null | 7.1+ |
: self / : static | Возврат текущего класса (для fluent) | 7.0+ / 8.0+ |
: User | Объект класса User | 7.0+ |
: iterable | array или объект Traversable | 7.1+ |
: never | Функция НЕ возвращает управление (throw / exit) | 8.1+ |
: int|string (union) | Несколько типов на выбор | 8.0+ |
: Countable&Iterator (intersection) | Реализует ВСЕ перечисленные | 8.1+ |
// 2. Альтернативный синтаксис if/foreach/while — для Blade-like шаблонов
<?php if ($user): ?>
<p>Hello, <?= $user->name ?></p>
<?php else: ?>
<p>Please log in.</p>
<?php endif; ?>
// Аналогично: foreach: endforeach; while: endwhile; switch: endswitch;
// 3. switch / case
switch ($status) {
case 200: echo 'OK'; break;
default: echo 'unknown';
}
// 4. Тернарный оператор — двоеточие разделяет true/false ветки
$x = $age >= 18 ? 'adult' : 'minor';
// ↑ это двоеточие, не return type
// 5. Двойное двоеточие :: (scope resolution) — это уже ДРУГОЙ оператор:
User::MAX_AGE; // константа класса
User::create(...); // статический метод
parent::__construct(); // вызов родительского метода
// ↑ это два символа подряд (T_PAAMAYIM_NEKUDOTAYIM), не одинарное :
: в функции — это return type. : void = ничего не возвращает, : float = float, : ?int = int или null. Двоеточие — метка типа возврата, не оператор.
strict_types Декларация
declare(strict_types=1);
function processAge(int $age): string {
return "Age: " . $age;
}
// С strict_types=1 это выбросит TypeError
processAge("25"); // ERROR! Требуется int, не string
// Нужно явно преобразовать
processAge((int)"25"); // OK
Union Types и Nullable (PHP 8+)
declare(strict_types=1);
// Union Types - функция может принять int или string
function getUserId(int|string $id): int {
if (is_string($id)) {
$id = (int)$id;
}
return $id;
}
// Nullable типы
function findUser(int $id): ?User {
// Может вернуть User или null
return $user ?? null;
}
// Эквивалентно: User|null
Intersection Types (PHP 8.1+)
interface Serializable {}
interface Countable {}
// Параметр должен реализовать ОБА интерфейса
function processCollection(Serializable&Countable $data): void {
echo count($data);
}
Проверка типов
is_int($var); // Целое число?
is_float($var); // Число с плавающей точкой?
is_string($var); // Строка?
is_bool($var); // Boolean?
is_array($var); // Массив?
is_object($var); // Объект?
is_null($var); // null?
is_numeric($var); // Числовое значение или строка с числом?
is_callable($var); // Можно вызвать как функцию?
isset($var); // Переменная установлена и не null?
empty($var); // Переменная пуста? (0, "", false, null)
gettype($var); // Возвращает строку с типом
2. Строки
Синтаксис строк: Double vs Single Quote
// Single quotes - буквальные строки, никакой интерпретации
'Hello $name' // Выведет: Hello $name
// Double quotes - интерпретирует переменные и экранирующие символы
"Hello $name" // Выведет: Hello John (если $name = 'John')
"Line1\nLine2" // Новая строка работает
// Heredoc - многострочная строка с интерпретацией
$text = <<$name,
This is a multiline string.
EOT;
// Nowdoc - многострочная строка БЕЗ интерпретации (как single quotes)
$text = <<<'EOT'
Hello $name,
This is literal.
EOT;
Строковая интерполяция (String Interpolation)
$name = "Alice";
$data = ['age' => 30];
// Простая переменная
"Hello $name"
// Через фигурные скобки для сложных выражений
"Age: {$data['age']}" // Правильно
"Age: $data['age']" // ОШИБКА - неправильная интерпретация
// Использование {} для явности
"{$name}'s age is {$data['age']}"
Heredoc и Nowdoc — многострочные строки
// Синтаксис: <<<ИДЕНТИФИКАТОР (без пробелов после <<<)
$name = "Анна";
$text = <<<EOT
Привет, $name!
Это многострочный текст.
Поддерживает переносы и "кавычки" без экранирования.
EOT;
echo $text;
// Вывод:
// Привет, Анна!
// Это многострочный текст.
// Поддерживает переносы и "кавычки" без экранирования.
// Синтаксис: <<<'ИДЕНТИФИКАТОР' (в одинарных кавычках)
$name = "Анна";
$text = <<<'EOT'
Привет, $name!
Здесь $name не заменится на "Анна".
\n и \t останутся как текст.
EOT;
echo $text;
// Вывод:
// Привет, $name!
// Здесь $name не заменится на "Анна".
// \n и \t останутся как текст.
// Идентификатор: любой (EOT, HTML, SQL, JSON), любые буквы/цифры/подчёркивания,
// не должен начинаться с цифры. Принято — UPPERCASE.
// Heredoc: без кавычек вокруг идентификатора
$x = <<<HTML
<div>$content</div>
HTML;
// Nowdoc: с одинарными кавычками
$x = <<<'HTML'
<div>$content</div>
HTML;
// Распространённая ошибка — пробелы и переменная после <<<
$x = << $name // SYNTAX ERROR — нужен идентификатор без пробела
text
EOT;
// PHP 7.3+ — закрывающий идентификатор может иметь отступ
// (отступ автоматически удалится из всех строк):
$x = <<<EOT
Первая строка
Вторая строка
EOT;
// Содержимое будет "Первая строка\nВторая строка" — без ведущих пробелов
+--------------------------------+--------------+--------------+
| Особенность | Heredoc | Nowdoc |
+--------------------------------+--------------+--------------+
| Синтаксис открытия | <<<EOT | <<<'EOT' |
| Интерпретация переменных | да | нет |
| Escape-последовательности | да (\n, \t) | нет |
| Аналог в обычных строках | " " | ' ' |
| Когда использовать | шаблоны с | литеральный |
| | подстановкой | код, SQL, |
| | | regex |
+--------------------------------+--------------+--------------+
// 1. Heredoc для шаблонов с переменными — HTML, SQL с параметрами, письма
$body = <<<HTML
<h1>Привет, $userName!</h1>
<p>Ваш заказ #$orderId на сумму $total руб.</p>
HTML;
// 2. Nowdoc для литерального текста — примеры кода, regex, тексты с $
$exampleCode = <<<'PHP'
<?php
$user = User::find(1);
echo "Hello, $user->name!";
PHP;
// Все $ останутся как есть, для документации/туториалов — идеально
// 3. SQL — обычно Heredoc с параметрами или Nowdoc для статичного запроса
$query = <<<'SQL'
SELECT u.id, u.email
FROM users u
WHERE u.status = 'active'
AND u.created_at > '2024-01-01'
SQL;
// Не нужно экранировать одинарные кавычки строки SQL!
// 4. JSON-шаблон
$json = <<<'JSON'
{
"name": "value",
"nested": {"key": 123}
}
JSON;
// 1. Закрывающий идентификатор в начале строки (до PHP 7.3)
// Любые пробелы или табы перед ним — Parse error
// С 7.3+ — отступ разрешён, но автоматически удаляется
$x = <<<EOT
text
EOT; // 7.2: SYNTAX ERROR; 7.3+: OK
// 2. После закрывающего идентификатора — только ; или ничего
$x = <<<EOT
text
EOT . "more"; // SYNTAX ERROR в PHP < 7.3
$x = <<<EOT
text
EOT;
$x .= "more"; // — конкатенация после
// 3. Идентификатор содержится в тексте — закроется раньше времени
$x = <<<EOT
Сообщение содержит слово EOT внутри текста
EOT;
// Текст оборвётся на первом EOT в начале строки.
// Решение: использовать уникальный идентификатор, не встречающийся в тексте.
// 4. Heredoc внутри функции/массива до PHP 7.3 требовал ; в конце
$arr = [<<<EOT
text
EOT
]; // 7.2: SYNTAX ERROR; 7.3+: OK (трейлинг ; можно опустить)
// 5. $ в Nowdoc — не нужно экранировать (в отличие от Heredoc)
$h = <<<EOT
Цена: \$100 // нужно экранировать $, иначе PHP попытается найти $100
EOT;
$n = <<<'EOT'
Цена: $100 // в Nowdoc — экранировать не нужно
EOT;
<<<EOT (Heredoc). Если текст — литеральный код, regex, пример с $ — <<<'EOT' (Nowdoc). Идентификатор — UPPERCASE, уникальный для содержимого (HTML, SQL, JSON, EOT). С PHP 7.3+ закрывающий маркер можно делать с отступом — используйте это для читаемости в методах классов.
PHP 8+ Функции строк
// Проверить содержит ли строка подстроку
str_contains("Hello World", "World"); // true
// Проверить начало строки
str_starts_with("https://example.com", "https"); // true
// Проверить конец строки
str_ends_with("file.pdf", ".pdf"); // true
// Практический пример - валидация URL в Laravel контроллере
if (!str_starts_with($url, "https://")) {
return response()->json(['error' => 'Invalid URL']);
}
Многобайтовые строки (mb_string)
$text = "Привет мир";
// Обычные функции работают с БАЙТАМИ, не с символами!
strlen("Привет"); // 12 (3 байта на каждый кириллический символ)
// mb функции работают с СИМВОЛАМИ
mb_strlen("Привет"); // 6 (6 символов, UTF-8)
mb_substr($text, 0, 3); // "При" (3 символа, не байта)
mb_strtoupper($text); // "ПРИВЕТ МИР"
mb_strtolower($text); // "привет мир"
mb_convert_case($text, MB_CASE_TITLE); // Первая буква заглавная
Регулярные выражения (Regex)
// preg_match - найти первое совпадение
if (preg_match('/^\d{3}-\d{3}-\d{4}$/', $phone)) {
// Телефон в формате XXX-XXX-XXXX
}
// preg_match с захватывающими группами
preg_match('/(\w+)@(\w+\.\w+)/', "test@example.com", $matches);
// $matches[0] = "test@example.com" (полное совпадение)
// $matches[1] = "test" (первая группа)
// $matches[2] = "example.com" (вторая группа)
// preg_match_all - найти ВСЕ совпадения
preg_match_all('/\d+/', "1 2 3 4 5", $numbers);
// $numbers[0] = ["1", "2", "3", "4", "5"]
// preg_replace - замена по паттерну
$clean = preg_replace('/[^a-zA-Z0-9]/', '', $input);
// Удаляет все символы кроме букв и цифр
// Практический пример - валидация email в Laravel
if (!preg_match('/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $email)) {
// Некорректный email
}
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ проверяет, похож ли текст в переменной $email на стандартный email-адрес вида username@domain.zone. Проверяет только формат, не существование ящика на сервере.
/ — начало и конец регулярки (разделитель)
^ — начало строки: проверка с первого символа
[a-zA-Z0-9._%+-]+ — локальная часть (то, что ДО @):
любые буквы, цифры, точки, подчёркивания, плюс, процент, дефис
+ означает «один или более раз»
примеры: ivan, i.van, ivan+spam, ivan_123
@ — символ @ (ровно один)
[a-zA-Z0-9.-]+ — имя домена (после @ до точки): gmail, yandex, my-site
\. — ЭКРАНИРОВАННАЯ точка — буквальный символ "."
[a-zA-Z]{2,} — доменная зона: только буквы, минимум 2 (com, ru, org)
$ — конец строки: после доменной зоны ничего быть не должно
/ — закрытие регулярки
. особый — он означает «любой символ» (кроме перевода строки). Чтобы искать настоящую точку (как между example и com), её экранируют обратным слешем: \.
Разница:
\.— ищет именно символ..— ищет любой символ: букву, цифру, запятую и т.д.
$email = "john.doe@example.com";
// Пошаговое сопоставление:
// ^ → начинаем с первого символа: j
// [a-zA-Z0-9._%+-]+ → берёт "john.doe" (точка внутри локальной части разрешена)
// @ → находит символ @
// [a-zA-Z0-9.-]+ → берёт "example"
// \. → находит буквальную точку
// [a-zA-Z]{2,} → берёт "com" (3 буквы, минимум 2)
// $ → строка закончилась → match
// → preg_match вернёт 1 (true)
// preg_match() возвращает:
// 1 — строка соответствует шаблону
// 0 — НЕ соответствует
// false — ошибка в самом шаблоне
// ! инвертирует результат:
if (!preg_match('/.../', $email)) {
// сюда попадаем, когда email НЕ соответствует формату
// → можно бросить ValidationException
}
// ivan@example — нет точки и доменной зоны (\. не найден)
// ivan@.com — домен пустой (между @ и точкой ничего нет)
// ivan@example.c — доменная зона "c" короче 2 символов
// ivan@example.com. — лишняя точка в конце (нарушает $)
// ivan#example.com — нет символа @
'email' => 'required|email:rfc,dns' — правило email:rfc проверяет полную спецификацию, dns ещё и резолвит домен.
preg_match_all('/\d+/', "1 2 3 4 5", $numbers);
// Разбор паттерна:
// \d — один любой цифровой символ (0-9)
// + — один или более раз подряд
// \d+ — последовательность из одной или нескольких цифр
// Функция идёт по строке слева направо и берёт куски, подходящие под \d+:
// "1" — пробел — стоп → совпадение
// "2" — пробел — стоп → совпадение
// "3", "4", "5" — то же
// Результат в $numbers (двумерный массив):
$numbers[0] === ["1", "2", "3", "4", "5"];
// Функция возвращает количество совпадений (целое число):
$count = preg_match_all('/\d+/', "1 2 3 4 5", $numbers);
echo $count; // 5
$numbers[0]— массив всех полных совпадений$numbers[1],$numbers[2]... — подсовпадения захватывающих групп (скобок)(...). Если групп в паттерне нет — этих индексов тоже нет.
preg_match_all('/\d+/', "abc 123 def 45 6", $numbers);
// $numbers[0] = ["123", "45", "6"]
// Пробелы и буквы проигнорированы; "123" — три цифры подряд считаются одним совпадением.
preg_match_all('/(\w+)@(\w+\.\w+)/', "a@x.com b@y.org", $m);
// $m[0] = ["a@x.com", "b@y.org"] — полные совпадения
// $m[1] = ["a", "b"] — первая группа (то, что в первых скобках)
// $m[2] = ["x.com", "y.org"] — вторая группа
\d — цифра, \D — не цифра\w — буква/цифра/подчёркивание, \W — всё остальное\s — пробельный символ (пробел/таб/перевод), \S — не пробельный. — любой символ, \. — буквальная точка+ — 1 или больше, * — 0 или больше, ? — 0 или 1{n} — ровно n раз, {n,m} — от n до m, {n,} — n или больше^ — начало строки, $ — конец строки[...] — набор символов, [^...] — кроме набора(...) — захватывающая группа, (?:...) — группа без захвата
sprintf и форматирование строк
// sprintf возвращает отформатированную строку
$formatted = sprintf("Hello %s, you are %d years old", "Alice", 30);
// "Hello Alice, you are 30 years old"
// printf выводит прямо
printf("Price: $%.2f", 19.5); // "Price: $19.50"
// Форматы:
// %s - string
// %d - integer
// %f - float (по умолчанию 6 знаков после запятой)
// %.2f - float с 2 знаками после запятой
// %x - hex
// %b - binary
// Практический пример - форматирование цены
$price = sprintf("$%.2f", 99.5); // "$99.50"
3. Массивы углублённо
Что такое массив. Индексированный vs ассоциативный
array для всего: и список, и словарь. Различают индексированный и ассоциативный по тому, какие ключи использованы.
$list = ["яблоко", "банан", "апельсин"];
// PHP под капотом сгенерировал ключи 0, 1, 2:
// [ 0 => "яблоко", 1 => "банан", 2 => "апельсин" ]
echo $list[0]; // "яблоко"
echo $list[1]; // "банан"
// Использование: списки, упорядоченные коллекции, где
// позиция элемента имеет значение (история, очередь, лог).
$person = [
"name" => "Иван",
"age" => 30,
"city" => "Москва",
];
echo $person["name"]; // "Иван"
// Ключ — это имя, оно несёт смысл. Не позиция.
// Ключи могут быть смешанными:
$mixed = [1 => "один", "key" => "value"];
// Использование: конфиги, DTO, данные из формы/БД/JSON,
// где у каждого поля есть осмысленное имя.
От «ассоциация» — связь, соответствие между двумя объектами. В ассоциативном массиве каждый ключ явно связан (ассоциирован) со своим значением — ключ означает что-то содержательное, а не просто порядковый номер.
Сравнение:
- Индексированный:
0 → "яблоко",1 → "банан"— связь только по порядку. - Ассоциативный:
"name" → "Иван"— ключ"name"ассоциирован со значением"Иван".
В других языках ассоциативные массивы называются: словарь (Python dict), хеш (Ruby Hash), map (Java HashMap), объект (JavaScript {}).
array// PHP внутри хранит массив как упорядоченную hash-таблицу.
// Поэтому ИНДЕКСИРОВАННЫЙ массив — это всего лишь
// ассоциативный с автогенерированными числовыми ключами.
$a = ["x", "y", "z"];
// Эквивалентно:
$a = [0 => "x", 1 => "y", 2 => "z"];
// Поэтому функции типа array_keys, array_values, foreach
// работают одинаково для обоих видов:
array_keys($a); // [0, 1, 2]
array_keys($person); // ["name", "age", "city"]
// Проверить, "индексированный" массив (PHP 8.1+):
array_is_list($a); // true (ключи 0..n-1 по порядку)
array_is_list($person); // false
- Индексированный — последовательность однотипных элементов:
$users = [User1, User2, User3], лог-записи, результаты выборки. - Ассоциативный — структурированные данные с именованными полями: ответ API, ряд из БД, конфиг.
Ловушка JSON: при json_encode индексированный массив становится JSON-массивом [...], ассоциативный — JSON-объектом {...}. Если случайно «продырявить» индексированный (unset($a[1])) — он станет ассоциативным [0=>"x", 2=>"z"] и JSON будет уже объектом, а не массивом. Лечится array_values().
Оператор => — все 6 контекстов использования
=> (его внутреннее имя — T_DOUBLE_ARROW) встречается в шести разных контекстах. Это не оператор сравнения (для этого ==, ===) и не общая «стрелка» как в JavaScript. Семантика в каждом контексте разная, но синтаксис один.
$arr = [
'name' => 'Иван',
'age' => 30,
5 => 'пятый', // ключ может быть числом
];
=> внутри литерала vs = при добавлении в массив.
Это два разных синтаксиса для разных операций:
// ─── Создание массива (литерал) — внутри [...] нужен => ───
$arr = ['name' => 'Иван', 'age' => 30];
// ─── Добавление элемента в существующий массив — снаружи [...] нужен = ───
$arr['email'] = 'ivan@x.kz'; // ключ в [], значение через =
$arr[5] = 'пять'; // верно
// ─── Синтаксические ошибки ───
$arr['name' => 'Иван']; // SYNTAX ERROR: внутри [] только ключ
$arr['name'] => 'Иван'; // SYNTAX ERROR: => не оператор присваивания
| Конструкция | Что делает | Валидно? |
|---|---|---|
[1, 2, 3] | Литерал массива без ключей | |
['a' => 1, 'b' => 2] | Литерал с ключами (=> ВНУТРИ []) | |
$arr[$key] = $value | Присваивание элементу (= СНАРУЖИ []) | |
$arr[] = $value | Добавление в конец (PHP сам генерит ключ) | |
$arr[$key] => $value | Попытка использовать => как оператор присваивания | syntax error |
$arr[$key => $value] | Попытка втиснуть пару в индексатор | syntax error |
- Внутри
[ ]при создании литерала → пишется=>(связка ключ-значение). - Снаружи
[ ]при добавлении/изменении → пишется=(присваивание).
Типичный кейс в array_reduce: $carry[$user['id']] = $user['name'] — добавляем элемент в накапливаемый массив $carry. Не $carry[$user['id'] => $user['name']] — это попытка создать литерал внутри индексатора, что синтаксически невозможно.
foreach ($arr as $key => $value) {
echo "$key => $value";
}
// Без => получили бы только значения:
// foreach ($arr as $value) { ... }
// fn($x) => выражение — короткая запись анонимной функции.
// => здесь заменяет return и фигурные скобки.
// Длинная форма:
$squared = array_map(function($n) {
return $n * $n;
}, $numbers);
// Стрелочная (короткая):
$squared = array_map(fn($n) => $n * $n, $numbers);
// Особенности стрелочных функций:
// — тело только ОДНО выражение (не несколько строк, не {})
// — автоматически захватывают внешние переменные (без use)
$multiplier = 3;
$tripled = array_map(fn($n) => $n * $multiplier, $numbers);
// $multiplier "пойман" автоматически — в обычной function требовался бы use ($multiplier)
// => разделяет проверяемое значение и возвращаемый результат
$result = match($status) {
200 => 'OK',
404 => 'Not Found',
500, 502, 503 => 'Server Error',
default => 'Unknown',
};
// В отличие от switch:
// — строгое сравнение (===), не нестрогое
// — возвращает значение (можно присвоить в переменную)
// — обязателен default или match попадание (иначе UnhandledMatchError)
function gen(): Generator {
yield 'a' => 1;
yield 'b' => 2;
yield 'c' => 3;
}
foreach (gen() as $k => $v) {
echo "$k: $v\n";
}
// Без => в yield генератор выдаёт автоматические числовые ключи 0, 1, 2...
// Распаковка ассоциативного массива в переменные с указанием ключей
$user = ['name' => 'Анна', 'age' => 25, 'role' => 'admin'];
['name' => $name, 'age' => $age] = $user;
// $name = 'Анна', $age = 25 (role игнорируется)
// До PHP 7.1 — только позиционная (без ключей):
// [$first, $second, $third] = $array;
(object)["a"=>1, "b"=>2]// Это два шага:
// 1. Сначала [] с => создаёт ассоциативный массив:
// ["a" => 1, "b" => 2]
// 2. (object) приводит массив к объекту — получается stdClass,
// где ключи становятся публичными свойствами:
$obj = (object)["a" => 1, "b" => 2];
// Эквивалентно:
$obj = new stdClass();
$obj->a = 1;
$obj->b = 2;
// Доступ:
echo $obj->a; // 1
// (через -> потому что это объект, не массив)
// Ключи с пробелами или цифровые становятся "магическими":
$obj = (object)["first name" => "Alice", 0 => "zero"];
echo $obj->{"first name"}; // "Alice" — через {} для невалидных имён
// $obj->0 — SYNTAX ERROR, тоже только через $obj->{0}
stdClass для DTO/конфига без объявления класса; работа с JSON (json_decode($json) без true возвращает именно stdClass — структура та же); миграция данных между разными API. Для production-кода лучше явные классы — IDE подсказывает поля, видны опечатки.
// Не оператор сравнения — для этого == или ===
$a => $b // SYNTAX ERROR
// Не стрелочный метод классов (в PHP такого синтаксиса нет)
class User {
public name => "default"; // SYNTAX ERROR
}
// Не используется в enum для задания значения — там просто =
enum Status: string {
case DRAFT = 'черновик'; // = (одно равно), не =>
case DRAFT => 'черновик'; // SYNTAX ERROR
}
=>| Контекст | Пример | Семантика | Версия PHP |
|---|---|---|---|
| Ассоциативный массив | ['key' => 'value'] | Связывает ключ со значением | любая |
foreach с ключами | foreach ($arr as $k => $v) | Перебор: ключ и значение | любая |
| Стрелочная функция | fn($x) => $x * 2 | Заменяет return + {} | 7.4+ |
match-выражение | match($x) { 1 => 'one' } | Разделитель проверяемого и результата | 8.0+ |
yield в генераторе | yield $key => $value | Выдача пары ключ-значение | 5.5+ |
| Деструктуризация по ключам | ['key' => $var] = $arr | Извлечение по имени ключа | 7.1+ |
=> — это ассоциация / связь (ключ ↔ значение, аргумент ↔ результат). Не путать с:
->— обращение к свойству/методу объекта:$user->name,$db->query()::— обращение к статике/классу:User::all(),self::CONST==/===— сравнение (loose / strict)=</>=— операторы сравнения «меньше или равно» / «больше или равно»
=> всегда обозначает связь между двумя сторонами: «ключ ↔ значение» (в массивах, foreach, yield, деструктуризации) либо «вход ↔ выход» (в стрелочных функциях и match). Левая часть — что подаём, правая — что получаем или какое значение хранится.
array_map - Преобразование элементов
// Применить функцию к каждому элементу
$numbers = [1, 2, 3, 4];
$squared = array_map(fn($n) => $n * $n, $numbers);
// [1, 4, 9, 16]
// Преобразование пользователей в массив ID
$users = [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob']
];
$ids = array_map(fn($u) => $u['id'], $users);
// [1, 2]
// С объектами
$users = User::all(); // Laravel Collection
$names = array_map(fn($user) => $user->name, $users->toArray());
// Применить функцию к нескольким массивам одновременно
$a = [1, 2, 3];
$b = [10, 20, 30];
$result = array_map(fn($x, $y) => $x + $y, $a, $b);
// [11, 22, 33]
// При разной длине массивов — берётся длина САМОГО ДЛИННОГО,
// недостающие элементы дополняются null (часто источник багов).
// 1. Один массив + ассоциативный → КЛЮЧИ СОХРАНЯЮТСЯ
$users = ['a' => 100, 'b' => 200];
$doubled = array_map(fn($n) => $n * 2, $users);
// ['a' => 200, 'b' => 400] — ключи a, b сохранились
// 2. Несколько массивов → КЛЮЧИ СБРАСЫВАЮТСЯ на 0, 1, 2...
$result = array_map(fn($x, $y) => $x + $y, ['a' => 1], ['b' => 2]);
// [0 => 3] — оба ключа потеряны!
// 3. callback = null + один массив → ПРОСТО КОПИРУЕТ значения
$copy = array_map(null, [1, 2, 3]);
// [1, 2, 3]
// 4. callback = null + несколько массивов → ZIP (склейка попарно)
$zipped = array_map(null, [1, 2, 3], ['a', 'b', 'c']);
// [[1,'a'], [2,'b'], [3,'c']] — как Python zip()
// array_map "под капотом":
$result = [];
foreach ($numbers as $key => $value) {
$result[$key] = $callback($value);
}
// Когда foreach лучше:
// — нужны побочные эффекты (логирование, запись в БД)
// — внутри тела > 1 строки кода
// — нужно прервать цикл (break/continue) — у array_map нельзя
// Вариант 1: array_map + array_sum (2 прохода по массиву)
$total = array_sum(array_map(
fn(Item $i) => $i->price(),
$this->items
));
// Вариант 2: array_reduce (1 проход)
$total = array_reduce(
$this->items,
fn(float $carry, Item $i) => $carry + $i->price(),
0.0
);
// На малых данных (≤ 1000) разницы нет — выбирай по читаемости.
// На больших — reduce 2× быстрее (1 проход + нет промежуточного массива).
| Сценарий | Лучший выбор |
|---|---|
| Просто преобразовать массив (новый из старого) | array_map |
| Свернуть в одно значение (sum/max/build assoc) | array_reduce |
| map + sum/max/min | Сразу array_reduce (1 проход) |
| Простая сумма чисел | array_sum (не нужны map/reduce) |
| Побочные эффекты (log/insert) | foreach |
Нужен break / прерывание | foreach (у array_map нельзя) |
| Изменить массив in-place (по ссылке) | array_walk с &$item |
array_map:
- Ключи теряются при multi-array — если работаешь с assoc-массивом, проверь что используешь один массив.
- Разная длина массивов → дополняется
null. Если ожидал что массив будет короче — баг. - Нельзя прервать — `break` не работает. Если нужно остановить итерацию —
foreachили генератор. - Создаёт промежуточный массив — для дальнейшего
sum/maxпредпочтиarray_reduce.
array_filter - Фильтрация элементов
// Фильтровать массив по условию
$numbers = [1, 2, 3, 4, 5];
$evens = array_filter($numbers, fn($n) => $n % 2 == 0);
// [2, 4] (ключи сохранены!)
// Удалить null и пустые значения
$data = ['name' => 'Alice', 'age' => null, 'city' => 'NYC'];
$filtered = array_filter($data);
// ['name' => 'Alice', 'city' => 'NYC']
// Фильтровать только ключи
$data = ['user_id' => 1, 'admin_id' => 2, 'name' => 'Test'];
$ids = array_filter($data, fn($k) => str_ends_with($k, '_id'), ARRAY_FILTER_USE_KEY);
// ['user_id' => 1, 'admin_id' => 2]
// Практический пример - получить активных пользователей из БД
$users = User::all();
$active = array_filter(
$users->toArray(),
fn($user) => $user['is_active'] === true
);
$n % 2 == 0. Оператор % (modulo) — остаток от деления. $n % 2 вернёт 0 для чётных чисел и 1 для нечётных. Поэтому $n % 2 == 0 — проверка чётности. Аналогично $n % 3 == 0 — делится ли на 3; $n % 10 — последняя цифра числа.
$data = ['user_id' => 1, 'admin_id' => 2, 'name' => 'Test'];
// 1. По умолчанию — в callback передаётся только ЗНАЧЕНИЕ
array_filter($data, fn($value) => is_numeric($value));
// ['user_id' => 1, 'admin_id' => 2] — оставляет только числовые значения
// 2. ARRAY_FILTER_USE_KEY — в callback передаётся только КЛЮЧ
array_filter($data, fn($k) => str_ends_with($k, '_id'), ARRAY_FILTER_USE_KEY);
// ['user_id' => 1, 'admin_id' => 2] — оставляет ключи, заканчивающиеся на _id
// 3. ARRAY_FILTER_USE_BOTH — в callback передаются ОБА: value, key (именно в таком порядке!)
array_filter($data, fn($v, $k) => str_starts_with($k, 'user') && is_numeric($v), ARRAY_FILTER_USE_BOTH);
// ['user_id' => 1] — ключ начинается с "user" И значение числовое
array_keys + цикл. ARRAY_FILTER_USE_KEY упрощает паттерны вида «оставить только поля с префиксом» (часто — для очистки FormRequest от лишних ключей). USE_BOTH — когда условие зависит и от ключа, и от значения одновременно.
// В строке $users->toArray() метод toArray() — это НЕ глобальная функция (хелпер).
// Это метод экземпляра класса Illuminate\Support\Collection (или Eloquent\Model).
$users = User::all(); // возвращает Collection (объект)
$users->toArray(); // рекурсивно превращает коллекцию в обычный массив PHP
// Зачем .toArray()? array_filter — встроенная функция PHP, она принимает массив,
// а не Laravel-коллекцию. Поэтому коллекцию приводят к массиву.
// Но если вы остаётесь в мире Laravel — у Collection есть СВОИ методы filter() и where(),
// которые умнее и идиоматичнее:
// Менее идиоматично — выйти из Collection, использовать PHP-функцию:
$active = array_filter($users->toArray(), fn($u) => $u['is_active']);
// Лучше — остаться в Collection:
$active = $users->filter(fn($u) => $u->is_active);
// Ещё лучше — where() для простых проверок:
$active = $users->where('is_active', true);
// Если в итоге нужен массив — в конце .toArray():
$activeArray = $users->where('is_active', true)->toArray();
— Хелперы — глобальные функции:
collect(), dd(), view(), config(), route(), now(), auth(). Вызываются без объекта.— Методы — определены в классах, вызываются через
->: $users->toArray(), $user->save(), $collection->filter().Полный справочник хелперов — в KB_10 «Хелперы & методы».
array_reduce - Свёртка массива
// Свернуть массив в одно значение (сумма)
$numbers = [1, 2, 3, 4];
$sum = array_reduce($numbers, fn($carry, $item) => $carry + $item, 0);
// 10
// Построить ассоциативный массив из списка
$users = [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob']
];
$indexed = array_reduce($users, function($carry, $user) {
$carry[$user['id']] = $user['name'];
return $carry;
}, []);
// [1 => 'Alice', 2 => 'Bob']
// Вычислить статистику
$orders = [
['id' => 1, 'amount' => 100],
['id' => 2, 'amount' => 200],
['id' => 3, 'amount' => 150]
];
$stats = array_reduce($orders, function($carry, $order) {
$carry['total'] += $order['amount'];
$carry['count']++;
$carry['avg'] = $carry['total'] / $carry['count'];
return $carry;
}, ['total' => 0, 'count' => 0, 'avg' => 0]);
total, count, avg). Начальное значение задано сразу со всеми ключами и нулями, чтобы при первом обращении ($carry['total'] += ..., $carry['count']++) PHP не выдал warning «Undefined index».
+------+---------+-------------+------------------------------------------+
| Шаг | $order | До $carry | После $carry |
+------+---------+-------------+------------------------------------------+
| init | — | — | total=0, count=0, avg=0 |
| 1 | 100 | t=0, c=0 | t=0+100=100, c=1, avg=100/1=100 |
| 2 | 200 | t=100, c=1 | t=100+200=300, c=2, avg=300/2=150 |
| 3 | 150 | t=300, c=2 | t=300+150=450, c=3, avg=450/3=150 |
+------+---------+-------------+------------------------------------------+
Результат $stats:
[
'total' => 450,
'count' => 3,
'avg' => 150,
]
$total / $count). В коде выше он пересчитывается каждый шаг, чтобы после любой итерации в $carry['avg'] было актуальное текущее среднее. Полезно, если callback вызывается не только array_reduce'ом, или если нужна промежуточная статистика для логирования.
$total = 0;
$count = 0;
foreach ($orders as $order) {
$total += $order['amount'];
$count++;
}
$avg = $count > 0 ? $total / $count : 0; // защита от деления на ноль
$stats = ['total' => $total, 'count' => $count, 'avg' => $avg];
// Результат идентичен. Разница только в стиле:
// array_reduce — функциональный, всё внутри одного выражения
// foreach — императивный, легче дебажить
$orders пустой, цикл не пройдёт, callback не вызовется — $stats['avg'] останется 0 из начального значения. В foreach-варианте без проверки $count > 0 произошло бы DivisionByZeroError. Это редкий случай, когда array_reduce «бесплатно» защищает от пограничного бага.
[1, 2, 3, 4]:
+------+--------------+-------------------------+-------------+
| Шаг | $item | Вычисление $carry+$item | Новый $carry|
+------+--------------+-------------------------+-------------+
| init | — | — (начальное = 0) | 0 |
| 1 | 1 | 0 + 1 = 1 | 1 |
| 2 | 2 | 1 + 2 = 3 | 3 |
| 3 | 3 | 3 + 3 = 6 | 6 |
| 4 | 4 | 6 + 4 = 10 | 10 |
+------+--------------+-------------------------+-------------+
После последнего элемента array_reduce возвращает $carry → 10.
$carry и $item — имена, придуманные разработчиком, они могут быть любыми ($x, $y, $a, $b). Важен порядок: первым аргументом callback всегда получает аккумулятор, вторым — текущий элемент. Это зашито в реализацию array_reduce на уровне ядра PHP.
function my_array_reduce(array $array, callable $callback, $initial = null) {
$accumulator = $initial;
foreach ($array as $item) {
// Жёстко зашитый порядок: 1-й arg = аккумулятор, 2-й = элемент
$accumulator = $callback($accumulator, $item);
}
return $accumulator;
}
$sum = 0;
foreach ($numbers as $item) {
$sum += $item;
}
// результат тот же: 10
// array_reduce — более декларативный способ, особенно для построения структур
— Если массив пустой →
array_reduce вернёт начальное значение (для 0 это логичная сумма, для 1 — произведение).— Если массив не пустой → начальное значение участвует в первом вычислении (
0 + 1 = 1 в нашем примере).Без начального значения по умолчанию —
null, что часто ломает callback (null + 1 = warning + 1).
// Для типовых операций PHP предоставляет специализированные функции —
// они короче, читаемее и БЫСТРЕЕ array_reduce (реализованы на C).
// Избыточно — переизобретаешь array_sum:
$sum = array_reduce($numbers, fn($c, $i) => $c + $i, 0);
// Идиоматично:
$sum = array_sum($numbers);
// Полный список:
array_sum($arr); // сумма
array_product($arr); // произведение
count($arr); // количество элементов
max($arr); // максимум
min($arr); // минимум
array_unique($arr); // убрать дубли
implode(',', $arr); // склеить в строку
// Среднее (avg) встроенной нет, но просто:
$avg = array_sum($arr) / count($arr); // защити count > 0!
| Задача | Использовать | Почему |
|---|---|---|
| Сумма | array_sum($arr) | Встроено, быстро, читаемо |
| Произведение | array_product($arr) | Встроено |
| Max / Min | max($arr) / min($arr) | Встроено |
| Среднее (avg) | array_sum / count | Двух функций достаточно |
| Сумма + count + avg за один проход | array_reduce | 1 итерация вместо 2-3 |
Построить assoc [id => name] | array_reduce или array_column | Reduce — гибче, column — короче |
| Сложная агрегация (sum чётных + max нечётных) | array_reduce | Своя логика в callback |
| Любая нестандартная свёртка | array_reduce | Универсальный инструмент |
sum/product/max/min/count) — используй её. array_reduce — это универсальный fallback для случаев когда встроенной нет или нужно несколько метрик за один проход.
array_walk - Изменение элементов с побочными эффектами
// array_walk изменяет массив IN-PLACE (по ссылке!)
$data = [1, 2, 3];
array_walk($data, function(&$item) {
$item *= 2;
});
// $data = [2, 4, 6]
// С дополнительным параметром (userdata)
$prices = [10, 20, 30];
$tax = 0.1;
array_walk($prices, function(&$price, $key, $taxRate) {
$price = $price * (1 + $taxRate);
}, $tax);
// $prices = [11, 22, 33]
// ВАЖНО: & влияет ТОЛЬКО на тот параметр, перед которым стоит.
// В этом примере:
// &$price — изменение мутирует значение в исходном массиве
// $key — копия ключа; изменить его внутри функции невозможно
// $taxRate — копия userdata; присвоение внутри не повлияет на $tax снаружи
// Что меняется в исходном $prices:
// значения элементов (11, 22, 33)
// ключи остаются прежними (0, 1, 2) — их вообще нельзя поменять через array_walk
// структура массива (количество элементов, порядок) не меняется
// Сигнатура callback для array_walk:
// function(&$value, $key, $userdata)
// позиция 1: значение (можно с & для мутации)
// позиция 2: ключ (всегда копия)
// позиция 3+: $userdata из 3-го аргумента array_walk (опционально)
// Если поставить & перед $key — будет WARNING, потому что array_walk
// передаёт ключ всегда по значению:
array_walk($arr, function($value, &$key) {
$key = "new_key"; // бесполезно, ключи не меняются
});
// Чтобы изменить ключи — используйте array_combine + array_map или явный foreach.
// Отправить письма всем пользователям
$users = [
['name' => 'Alice', 'email' => 'alice@ex.com'],
['name' => 'Bob', 'email' => 'bob@ex.com']
];
array_walk($users, function($user) {
Mail::to($user['email'])->send(new WelcomeMail($user['name']));
});
& перед параметром (как в &$item). Символ & называется амперсанд, в PHP он означает передачу по ссылке (by reference). Без него параметр — это копия значения; изменения внутри функции не затронут оригинал. Со & — параметр становится псевдонимом самого исходного элемента; всё, что делается с переменной внутри функции, изменяет исходные данные.
// Без & — копия, оригинал не меняется
$data = [1, 2, 3];
array_walk($data, function($item) {
$item *= 10; // меняем КОПИЮ — бесполезно
});
print_r($data); // [1, 2, 3] — без изменений
// С & — ссылка, оригинал изменяется
$data = [1, 2, 3];
array_walk($data, function(&$item) {
$item *= 10; // меняем САМ элемент массива
});
print_r($data); // [10, 20, 30] — массив реально изменился
// Без & — без эффекта
function addOneCopy($num) {
$num++;
}
$x = 5;
addOneCopy($x);
echo $x; // 5 — $x не изменился
// С & — функция мутирует переданную переменную
function addOneRef(&$num) {
$num++;
}
$x = 5;
addOneRef($x);
echo $x; // 6 — $x изменился, потому что передан по ссылке
// 1. Параметр функции по ссылке (как выше)
function f(&$arg) { ... }
// 2. Возврат по ссылке (редко используется)
function &getRef() { return $this->data; }
// 3. Присваивание по ссылке — создание псевдонима
$a = 10;
$b = &$a; // $b — это другое имя для $a
$b = 20;
echo $a; // 20 — изменилось через $b
// 4. foreach по ссылке — изменение массива во время перебора
$arr = [1, 2, 3];
foreach ($arr as &$val) {
$val *= 10;
}
// $arr = [10, 20, 30]
// 5. Битовое И (другой контекст — оператор & между значениями, не префикс)
$flags = 0b1010 & 0b1100; // 0b1000
$arr = [1, 2, 3];
// Первый foreach по ссылке — изменяем массив
foreach ($arr as &$val) {
$val *= 10;
}
// После цикла $val ОСТАЁТСЯ ссылкой на последний элемент массива!
// Второй foreach — без &, но переиспользует ту же $val
foreach ($arr as $val) {
// каждая итерация ПЕРЕЗАПИСЫВАЕТ $arr[2] (последний элемент!)
// в результате: $arr = [10, 20, &$val=10], затем [10, 20, 20], затем [10, 20, 20]
}
print_r($arr); // [10, 20, 20] вместо ожидаемого [10, 20, 30]
// Решение: после foreach по ссылке всегда unset:
foreach ($arr as &$val) { $val *= 10; }
unset($val); // разорвать ссылку
&: когда явно нужно мутировать оригинал (array_walk, реализация Stack/Queue, оптимизация для огромных строк/массивов чтобы не копировать).Когда НЕ использовать: в чистых функциях (вход → выход, без побочек), в API-границах вашего модуля, при работе с объектами (объекты в PHP и так передаются «по handle» — ссылка не нужна), и всегда когда читателю кода будет неочевидно что переменная мутируется.
Правило большого пальца: явные мутации через возврат значения (
$arr = array_map(...)) почти всегда читаются лучше, чем неявные через &. Ссылку оставляйте для тех случаев, где return неудобен.
usort и сортировка массивов
// usort - сортирует массив с пользовательским компаратором
$users = [
['name' => 'Charlie', 'age' => 25],
['name' => 'Alice', 'age' => 30],
['name' => 'Bob', 'age' => 28]
];
// Сортировать по возрасту (возрастающе)
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// [Charlie, Bob, Alice] (25, 28, 30)
// Сортировать по возрасту (убывающе)
usort($users, fn($a, $b) => $b['age'] <=> $a['age']);
// Сортировать по нескольким параметрам (сначала возраст, потом имя)
usort($users, function($a, $b) {
$ageCompare = $a['age'] <=> $b['age'];
if ($ageCompare !== 0) {
return $ageCompare;
}
return $a['name'] <=> $b['name'];
});
// Сортировать объекты (Laravel Model)
$users = User::all();
usort($users->toArray(), fn($a, $b) => $a['created_at'] <=> $b['created_at']);
<=> — «spaceship» (космический корабль), PHP 7+. Трёхзначный оператор сравнения: возвращает целое число, показывающее как левая часть относится к правой. Это именно то, что хотят функции вроде usort, uasort, uksort, SplPriorityQueue и любые алгоритмы сортировки: «дай мне -1/0/1 и я разберусь».
// -1 — $a меньше $b
// 0 — $a равно $b
// 1 — $a больше $b
var_dump(5 <=> 10); // int(-1)
var_dump(5 <=> 5); // int(0)
var_dump(10 <=> 5); // int(1)
// Работает не только с числами:
var_dump("apple" <=> "banana"); // int(-1) — строки сравниваются лексикографически
var_dump("banana" <=> "apple"); // int(1)
// С массивами — поэлементно (size, потом keys, потом values):
var_dump([1, 2, 3] <=> [1, 2, 3]); // int(0)
var_dump([1, 2] <=> [1, 2, 3]); // int(-1) — меньше размер
// С объектами — обычно через __toString() или магию класса
// usort требует от callback вернуть:
// отрицательное число — если $a должен идти ПЕРЕД $b
// ноль — если порядок неважен (равны)
// положительное — если $a должен идти ПОСЛЕ $b
// До PHP 7 без <=> писали так:
usort($users, function($a, $b) {
if ($a['age'] < $b['age']) return -1;
if ($a['age'] > $b['age']) return 1;
return 0;
});
// С <=> — одна строка:
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// ВАЖНО: НЕ используйте $a - $b как компаратор!
// Работает для int, но ломается для float (теряется точность дробей)
// и для больших чисел (integer overflow). <=> — безопасно всегда.
usort($arr, fn($a, $b) => $a - $b); // хрупко
usort($arr, fn($a, $b) => $a <=> $b); // правильно
// По возрастанию (ASC): $a слева, $b справа
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// 25, 28, 30
// По убыванию (DESC): поменять местами $a и $b
usort($users, fn($a, $b) => $b['age'] <=> $a['age']);
// 30, 28, 25
// Альтернатива — умножить результат на -1 (читается хуже):
usort($users, fn($a, $b) => -($a['age'] <=> $b['age']));
// Сортировать сначала по возрасту, потом по имени.
// Идея: первое не-нулевое <=> и есть итоговый результат.
// ?: — короткий тернарный (Elvis): если левая часть truthy — её, иначе правую.
usort($users, fn($a, $b) =>
($a['age'] <=> $b['age'])
?: ($a['name'] <=> $b['name'])
);
// Логика:
// — если возрасты разные (<=> вернёт -1 или 1) — итог это значение
// — если равны (<=> вернёт 0, что falsy) — переходим к сравнению имён
// Для трёх критериев и больше:
usort($users, fn($a, $b) =>
($a['department'] <=> $b['department'])
?: ($b['salary'] <=> $a['salary']) // salary DESC (b vs a)
?: ($a['name'] <=> $b['name'])
);
// Сначала по отделу (ASC), внутри отдела по зарплате (DESC), внутри по имени (ASC)
usort — пользовательский компаратор, теряет ключи (переиндексирует 0..N).uasort — то же, но сохраняет ключи (Associative).uksort — сортирует по ключам, не по значениям.sort / rsort — быстрая сортировка без callback (ASC/DESC).ksort / krsort — по ключам без callback.В Laravel Collection:
$c->sortBy('age'), $c->sortByDesc('age'), $c->sortBy(fn($u) => ...) — идиоматично, сохраняет ключи.
array_column, array_combine и другие
// array_column - извлечь столбец из многомерного массива
$users = [
['id' => 1, 'name' => 'Alice', 'age' => 30],
['id' => 2, 'name' => 'Bob', 'age' => 28]
];
$names = array_column($users, 'name');
// ['Alice', 'Bob']
$indexed = array_column($users, 'name', 'id');
// [1 => 'Alice', 2 => 'Bob']
// array_combine - создать массив с ключами и значениями
$keys = ['a', 'b', 'c'];
$values = [1, 2, 3];
$result = array_combine($keys, $values);
// ['a' => 1, 'b' => 2, 'c' => 3]
// array_merge - объединить массивы
$arr1 = ['a' => 1];
$arr2 = ['b' => 2];
$merged = array_merge($arr1, $arr2);
// ['a' => 1, 'b' => 2]
// array_values и array_keys
$data = ['name' => 'Alice', 'age' => 30];
$values = array_values($data); // ['Alice', 30]
$keys = array_keys($data); // ['name', 'age']
Spread Operator и Destructuring
// Spread operator ... для распаковки массива
$arr1 = [1, 2];
$arr2 = [3, 4];
$merged = [...$arr1, ...$arr2];
// [1, 2, 3, 4]
// Распаковка в функцию
function sum($a, $b, $c) {
return $a + $b + $c;
}
$numbers = [1, 2, 3];
echo sum(...$numbers); // 6
// Array destructuring (распаковка в переменные)
[$first, $second] = [10, 20];
// $first = 10, $second = 20
// С ассоциативными массивами
['name' => $name, 'age' => $age] = ['name' => 'Alice', 'age' => 30];
// $name = 'Alice', $age = 30
// Пропустить элементы
[$first, , $third] = [1, 2, 3];
// $first = 1, $third = 3 (второй элемент пропущен)
// Практический пример - распаковать функцию в параметры
$userIds = [1, 2, 3];
User::whereIn('id', $userIds)->get();
4. ООП: Классы, Наследование, Полиморфизм
Класс vs Объект. Создание объекта через new
new.
// КЛАСС — это чертёж (описание): какие свойства и методы будут.
// Сам по себе класс не занимает память для хранения данных.
// Это просто текст программы.
class User {
public string $name;
protected int $id;
}
// ОБЪЕКТ — это конкретный дом, построенный по чертежу.
// В памяти выделено место под его свойства, в нём хранятся реальные значения.
$user1 = new User(); // первый объект класса User
$user2 = new User(); // второй объект — независимый
// $user1 и $user2 — РАЗНЫЕ объекты в памяти,
// хотя оба построены по одному и тому же классу User.
new ClassName() под капотом:
- Выделяет память для нового объекта (с местом под все объявленные свойства + служебная информация).
- Запускает
__construct(), если он определён в классе. - Возвращает ссылку на созданный объект (PHP всегда работает с объектами по ссылке, не по значению).
new User(), а не просто User?$array = (array)new User(); // объект → массив
$array = (array)User; // ошибка: класс не значение
// Почему так:
// User — это ИМЯ КЛАССА (тип данных), как int или string.
// Его нельзя использовать как переменную или значение.
// Это просто метка для PHP: "вот по такому описанию строить объекты".
// new User() — это ВЫРАЖЕНИЕ, которое возвращает объект.
// А с объектом уже можно делать что угодно: кастить, передавать, сохранять.
// Аналогично с другими операциями над типом:
var_dump(User); // ошибка
var_dump(new User()); // дамп объекта
// Имя класса как СТРОКА — можно (через ::class или просто "User"):
echo User::class; // "User" — это уже строка, не класс
echo get_class(new User()); // "User"
class User {
public string $name;
public int $age;
public function __construct(string $name, int $age)
{
$this->name = $name;
$this->age = $age;
}
}
// Конструктор вызывается автоматически — поля сразу заполнены
$user = new User("Alice", 30);
echo $user->name; // "Alice"
// Без конструктора пришлось бы заполнять руками после new:
// $user = new User();
// $user->name = "Alice";
// $user->age = 30;
| Аспект | Класс | Объект |
|---|---|---|
| Что это | Описание (чертёж, рецепт, тип) | Экземпляр (конкретный дом, пирог) в памяти |
| Память | Не выделена под данные | Выделена — хранит реальные значения свойств |
| Как «создать» | class User { ... } | new User(...) |
| Сколько может быть | Один (в namespace) | Сколько угодно из одного класса |
| Доступ к свойству | Нельзя (нет свойств у класса) | $user->name |
| Static-член | User::CONST, User::method() | принадлежит классу, не объекту |
| Тип в PHP | — | object (можно type-hint User) |
В var_dump | не дампится | object(User)#1 (2) {...} |
- Класс ≠ объект. Это первая ловушка для тех, кто пришёл из процедурного программирования.
- Оператор
new— единственный стандартный способ создать объект (есть ещё рефлексия черезReflectionClass::newInstance(), но это специфика). - Объект в PHP всегда передаётся по ссылке:
$b = $aделает$bи$aссылками на ОДИН объект. Изменение одного видно в другом. Для копии —clone $a. - Аналогия для запоминания: класс — рецепт пирога, объект — испечённый пирог. Рецепт можно прочитать, но съесть нельзя. Пирог можно есть, взвешивать, разрезать.
$this — ссылка на текущий объект
$this — псевдо-переменная, доступная внутри любого нестатического метода класса. Она ссылается на тот объект, у которого сейчас вызывается метод. Через $this->имя читаются и записываются свойства, через $this->метод() вызываются другие методы того же объекта.
class User {
public $name;
public $email;
public function __construct($name, $email) {
$this->name = $name; // свойство объекта = аргумент
$this->email = $email;
}
}
$user = new User("Alice", "alice@ex.com");
// Внутри __construct $this === $user (тот объект, который создаётся)
$name — локальная переменная (аргумент или просто объявленная в методе). $this->name — свойство объекта. Это разные сущности с одинаковым именем.
class Greeter {
public $name = "Default";
public function test($name) {
echo $name; // локальная переменная-аргумент
echo $this->name; // свойство объекта ("Default")
$this->name = $name; // присвоить аргумент в свойство
}
}
$g = new Greeter();
$g->test("Alice"); // выведет "AliceDefault", потом установит свойство
class Order {
private array $items = [];
public function addItem($item): void {
$this->items[] = $item;
}
public function total(): int {
return array_sum($this->items);
}
public function summary(): string {
// $this->total() — вызов другого метода ЭТОГО ЖЕ объекта
return "Items: " . count($this->items) . ", total: " . $this->total();
}
}
class QueryBuilder {
private array $where = [];
private ?int $limit = null;
public function where(string $col, $value): self {
$this->where[$col] = $value;
return $this; // ← возвращаем сам объект
}
public function limit(int $n): self {
$this->limit = $n;
return $this;
}
}
// Благодаря return $this — можно цепочкой:
$users = (new QueryBuilder())
->where('status', 'active')
->where('age', 18)
->limit(10);
// Так работают Laravel Query Builder, Eloquent, Collection, и пр.:
// User::where('id', 1)->with('orders')->first() — каждый метод возвращает $this
// 1. В static-методах — нет «текущего объекта», метод принадлежит классу
class Calculator {
public static function add(int $a, int $b): int {
return $a + $b;
// $this здесь — fatal error "Using $this when not in object context"
}
}
Calculator::add(2, 3); // вызов без объекта
// 2. В обычных функциях вне класса
function helper() {
echo $this; // fatal error
}
// 3. В замыканиях по умолчанию — но можно через Closure::bind
$closure = function() { echo $this->name; };
$closure(); // undefined $this
// Решение — bind замыкания к объекту:
$bound = Closure::bind($closure, $user, User::class);
$bound(); // "Alice"
// До PHP 8.0 — boilerplate с $this в конструкторе:
class User {
public $name;
public $email;
public function __construct($name, $email) {
$this->name = $name; // повторение 3 раза:
$this->email = $email; // объявление, аргумент, присвоение
}
}
// С PHP 8.0+ — promoted properties (объявление + присвоение в одной строке):
class User {
public function __construct(
public string $name,
public string $email,
) {} // тело пустое — PHP сам делает $this->name = $name
}
// Можно совмещать с readonly (PHP 8.1+) — иммутабельные DTO:
final class UserDto {
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
}
Внутри нестатических методов класса — обращайтесь к свойствам и методам через
$this->.В конструкторе —
$this->поле = $аргумент присваивает значение свойству объекта.return $this в setter'ах — даёт fluent interface (method chaining).В static-методах
$this отсутствует — используйте self:: или static:: для доступа к классу.В замыканиях по умолчанию нет — используйте
Closure::bind или стрелочные функции (fn() => $this->x внутри метода захватят $this автоматически).С PHP 8.0+ для конструкторов используйте promoted properties — короче и без повторов.
Когда писать $, а когда нет — все формы доступа к свойствам
private $items с долларом, а $this->items без?». PHP имеет 5 разных синтаксических форм работы с именем свойства/переменной. Разберём каждую.
| Форма | Что это | Где используется |
|---|---|---|
$items | Локальная переменная | Внутри функции/метода. Существует только в этом scope. |
private array $items = [] | Объявление свойства класса | Тело класса. $ здесь — часть имени переменной. |
$this->items | Доступ к свойству объекта | Внутри нестатического метода. $ только у $this, у items нет. |
$this->$varName | Variable property — имя свойства в переменной | Редкая магия / опасный паттерн. Если $varName = 'items', то $this->$varName = $this->items. |
self::$items | Static свойство | Внутри методов класса. У static свойств $ остаётся (отличие от обычных!). |
items (без $ и без ->) | Константа | PHP ищет const items. Если нет — warning + строка 'items' (deprecated, в PHP 8.0 → Error). |
class Cart
{
private array $items = []; // 1. ОБЪЯВЛЕНИЕ — $ есть (часть синтаксиса)
public static int $totalCarts = 0; // 2. static — $ есть и тут
public function addItem(Item $item): void
{
$this->items[] = $item; // 3. ДОСТУП — items БЕЗ $ (после ->)
self::$totalCarts++; // 4. static — $ ОСТАЁТСЯ после ::
$localCount = count($this->items); // 5. локальная переменная — $ есть
// 6. Variable property — магия (редко)
$field = 'items';
echo count($this->$field); // = $this->items, два $ намеренно
}
}
items без $ после -> — логика языка
У $this уже есть $ — это переменная, ссылающаяся на объект. Дальше идёт оператор ->, который говорит PHP: «следующий идентификатор — это имя свойства/метода объекта». Имя свойства — это просто идентификатор (как имя функции str_contains), $ не нужен.
Аналогия:
strlen('hello')— имя функцииstrlenбез$(это идентификатор)$this->items— имя свойстваitemsбез$(это идентификатор)self::$count— у static$остаётся (исторически, для отличия от константы классаself::COUNT)
$this->$field с двумя $
Это не опечатка, а variable property — обращение к свойству, имя которого хранится в переменной $field.
$this->items // обращение к свойству items $this->$items // variable property — ищет свойство с именем из $items // (если $items = 'name', то это = $this->name)
В реальном коде встречается редко (динамические DTO, рефлексия). 99% случаев — это опечатка: программист случайно написал лишний $. Поведение: если переменная не определена / содержит несуществующее имя — fatal error.
- Объявление свойства:
private $x→$ЕСТЬ - Доступ через объект:
$this->x→$НЕТ (после->) - Доступ к static:
self::$x→$ЕСТЬ (после::) - Через переменную (variable property):
$this->$x→ ДВА$— редко, обычно опечатка - Локальная переменная в методе:
$x→$ЕСТЬ (это обычная переменная)
Частый вопрос: «что значит $this->$x с двумя долларами?» — это variable property. 90% разработчиков забывают про это.
Static, self, static:: — классовые члены
:: без new.
self и static — два способа сослаться на класс изнутри его кода. Разница между ними проявляется при наследовании.
class Counter {
public static $count = 0; // статическое свойство
public static function increment(): void {
self::$count++; // доступ через self::
}
public static function getCount(): int {
return self::$count;
}
}
// Вызов через :: без new — объект не создаётся
Counter::increment();
Counter::increment();
echo Counter::getCount(); // 2
// Counter::$count живёт на уровне класса.
// Если бы это было обычное свойство — у каждого объекта свой счётчик.
// Со static — один счётчик на всё приложение.
// Доступ через :: применяется для:
// 1. Статических методов и свойств
ClassName::staticMethod();
ClassName::$staticProperty;
// 2. Констант класса (всегда через ::, даже у объекта)
ClassName::CONSTANT_NAME;
User::STATUS_ACTIVE;
// 3. Псевдо-классов: self::, parent::, static::
self::method(); // текущий класс (раннее связывание)
parent::method(); // родительский класс
static::method(); // фактический класс при вызове (LSB)
// 4. ::class — получить полное имя класса как строку (PHP 5.5+)
echo User::class; // "App\Models\User"
// Историческое название "::" — Paamayim Nekudotayim (ивр. «двойное двоеточие»);
// если PHP выдаст ошибку "Paamayim Nekudotayim" — это синтаксическая ошибка с ::
class ParentClass {
public static function testSelf(): string {
return self::who(); // раннее связывание
}
public static function testStatic(): string {
return static::who(); // позднее (Late Static Binding)
}
public static function who(): string {
return __CLASS__; // магическая константа: имя текущего класса
}
}
class ChildClass extends ParentClass {
public static function who(): string {
return __CLASS__;
}
}
echo ChildClass::testSelf(); // "ParentClass" ← зафиксировано в parent
echo ChildClass::testStatic(); // "ChildClass" ← фактический класс вызова
// Объяснение:
// self::who() в ParentClass — намертво привязано к ParentClass.
// Кто бы ни вызывал — всегда parent's who().
// static::who() — динамически определяется во время выполнения.
// Если вызвано через ChildClass:: — найдёт ChildClass::who().
// Это и есть Late Static Binding (LSB), появился в PHP 5.3.
ChildClass::testSelf(). Метод testSelf() определён только в ParentClass. Внутри него — вызов self::who(). self — это «класс, в котором физически написан этот код», то есть ParentClass. Поэтому PHP идёт в ParentClass и берёт who() именно оттуда (возвращает 'Parent'), игнорируя переопределённый who() в ChildClass.
┌─────────────────────────────────────────────────────────────┐
│ 1. PHP видит вызов: ChildClass::testSelf() │
│ 2. Ищет метод testSelf() в ChildClass — НЕТ │
│ 3. Поднимается к родителю ParentClass — НАШЁЛ │
│ 4. Выполняет код тела: return self::who(); │
│ 5. self → ParentClass (класс ГДЕ ЭТОТ КОД написан) │
│ 6. Вызывает ParentClass::who() → возвращает 'Parent' │
│ 7. Итог: echo выводит "Parent" │
└─────────────────────────────────────────────────────────────┘
Ключевая мысль: self смотрит на ТЕКСТ кода (где написано),
а не на КЛАСС, через который сделан вызов.
Метод "переехал" в ChildClass через наследование, но self
по-прежнему указывает на ParentClass — потому что строчка
self::who() физически находится в файле ParentClass.
┌─────────────────────────────────────────────────────────────┐
│ 1. Вызов: ChildClass::testStatic() │
│ 2. testStatic() найден в ParentClass (унаследован) │
│ 3. Выполняется: return static::who(); │
│ 4. static → ЧТО было слева от :: при вызове = ChildClass │
│ 5. Ищет who() в ChildClass — НАШЁЛ (переопределён) │
│ 6. Вызывает ChildClass::who() → возвращает 'Child' │
│ 7. Итог: echo выводит "Child" │
└─────────────────────────────────────────────────────────────┘
Ключевая разница: static — это «класс, через который начался вызов»,
а не класс, где написан код.
PHP запоминает ChildClass с шага 1
и подставляет его в static:: на шаге 4.
self— как почтовый адрес отправителя на конверте: указан автором письма, не меняется кто бы письмо ни читал. Адрес = «где я писал».static— как каждый раз заново звонящий курьер: смотрит на наклейку «кому» прямо сейчас. Адрес = «кто вызвал».
class ChildClass extends ParentClass {
// Если переопределить testSelf() здесь:
public static function testSelf(): string {
return self::who(); // теперь self = ChildClass!
}
public static function who(): string {
return 'Child';
}
}
echo ChildClass::testSelf(); // "Child"
// Потому что self::who() теперь написан внутри ChildClass.
// Главное правило: self указывает на класс, где БУКВАЛЬНО лежит строчка с self.
static::. Метод getTableLate() ведёт себя по-разному в зависимости от того, через какой класс его вызвали, даже если сам определён в родителе. Достигается за счёт static:: — он указывает на класс, через который произошёл вызов во время выполнения (Late Static Binding). С обычным self:: такого эффекта нет: значение всегда берётся из класса, где написан код.
class BaseModel {
public static $table = 'base';
public static function getTable(): string {
return self::$table; // self → всегда BaseModel
}
public static function getTableLate(): string {
return static::$table; // static → класс, через который вызвано
}
}
class UserModel extends BaseModel {
public static $table = 'users'; // переопределили
}
echo BaseModel::getTable(); // "base" — self::$table → BaseModel::$table
echo BaseModel::getTableLate(); // "base" — static = BaseModel (вызвали от BaseModel)
echo UserModel::getTable(); // "base" self заблокирован на BaseModel
echo UserModel::getTableLate(); // "users" static подхватил UserModel::$table
+--------------------------------+------+--------------+----------+
| Вызов | self | static | Результат|
+--------------------------------+------+--------------+----------+
| BaseModel::getTable() | Base | (не использ) | "base" |
| BaseModel::getTableLate() | — | BaseModel | "base" |
| UserModel::getTable() | Base | (не использ) | "base" |
| UserModel::getTableLate() | — | UserModel | "users"|
+--------------------------------+------+--------------+----------+
Колонка "self" — какой класс PHP подставляет под self при выполнении
(всегда тот, где написан код = BaseModel).
Колонка "static" — какой класс подставляется под static
(тот, через который начали вызов).
UserModel::getTable() возвращает "base" — частая ловушка.
Программист переопределил $table в UserModel, ожидая что метод
подхватит новое значение. Но getTable() жёстко смотрит на
BaseModel::$table из-за self.
Чтобы наследники работали полиморфно — пишите static:: в базовом
классе для всего, что наследники могут переопределять.
Model::find($id) внутри использует new static(), а свойства типа $table, $primaryKey, $fillable читаются через static::. Это позволяет наследникам (User extends Model) переопределить $table = 'users' — и базовые методы родителя автоматически начинают работать с правильной таблицей. Без LSB пришлось бы переопределять каждый метод в каждом наследнике.
self:: → НЕ полиморфно: всегда берёт из класса, где написан код.
Удобно когда вы УВЕРЕНЫ что наследники не переопределят.
static:: → ПОЛИМОРФНО: берёт из класса, через который вызвали.
Используйте в базовом классе для всего, что наследники
могут менять: $table, $primaryKey, фабричные new static(),
override-методы вроде find/all/firstOrFail.
// В Eloquent почти все статические методы используют static:: чтобы работать с наследниками
class Model {
public static function find(int $id): ?static {
// псевдо-код: достать из БД
$row = DB::findRow($id);
return $row ? new static($row) : null;
// ↑ new static() — создаёт экземпляр фактического класса
}
}
class User extends Model {}
class Admin extends User {}
$user = User::find(1); // возвращает User
$admin = Admin::find(5); // возвращает Admin (не User!)
// Если бы в find() было new self() — всегда бы возвращался Model.
// new static() работает полиморфно — это ключ к Eloquent-фабрикам.
class Animal {
public function describe(): string {
return "Animal";
}
}
class Dog extends Animal {
public function describe(): string {
return parent::describe() . " → Dog";
// parent:: — явный вызов реализации родителя
// Полезно когда переопределяешь метод, но хочешь сохранить базовое поведение
}
}
(new Dog())->describe(); // "Animal → Dog"
// В конструкторах parent::__construct() — стандартный способ
// инициализировать родителя перед своей логикой:
class SpecialUser extends User {
public function __construct(string $name, int $level) {
parent::__construct($name); // инициализация родителя
$this->level = $level;
}
}
class Order {
const STATUS_PENDING = 'pending';
const STATUS_PAID = 'paid';
// PHP 7.1+ — у констант есть видимость:
private const MAX_RETRIES = 3;
// PHP 8.3+ — typed constants:
const int VERSION = 2;
}
// Доступ через :: (даже у объекта — не через ->):
echo Order::STATUS_PENDING; // "pending"
$order = new Order();
echo $order::STATUS_PAID; // "paid" (тоже через :: после объекта)
// echo $order->STATUS_PAID; — ошибка, через -> только свойства
// Утилитарные функции без состояния
class StringHelper {
public static function slugify(string $text): string {
return strtolower(str_replace(' ', '-', $text));
}
}
// Фабричные методы (named constructors)
class Date {
public static function today(): static { return new static(date('Y-m-d')); }
public static function fromString(string $s): static { return new static($s); }
}
// Константы (как enum-замена до PHP 8.1)
class HttpStatus {
const OK = 200;
const NOT_FOUND = 404;
}
// НЕ для сервисов с состоянием — таблица плюсов/минусов:
// Static-сервис: Обычный сервис через DI:
// — невозможно мокать в тестах + легко мокать
// — глобальное состояние + изолированный state
// — нельзя подменить реализацию + interface + DI
// + не надо передавать через DI — нужно DI-контейнер
// В Laravel почти НИКОГДА не используют свои static-сервисы — все через
// контейнер. Static резервируется для фабрик (Model::find), хелперов
// и фасадов (хотя фасады тоже под капотом обращаются к контейнеру).
// 1. $this в static-методе → Fatal Error
class X {
public static function f() {
echo $this->name; // Fatal: Using $this when not in object context
}
}
// 2. Свойства через -> вместо ::
Counter::$count++; // правильно
$counter = new Counter();
$counter->count++; // ошибка — у объекта нет такого свойства,
// статическое живёт на классе
// 3. Static в Octane / RoadRunner / Swoole — утечка состояния
// В обычном PHP-FPM статика сбрасывается с окончанием процесса.
// В long-running (Octane) статика живёт между запросами:
class UserCache {
private static array $cache = []; // под Octane накапливается!
}
// Подробнее в KB_3 (Laravel → Octane).
// 4. self vs static в final-классах
// Если класс final (нельзя наследовать) — self и static идентичны.
// Разница только при наследовании.
// 5. Тестирование static — почти невозможно мокать
// StringHelper::slugify($x) в коде → в тестах нельзя подменить.
// Решение: сделать обычный сервис + interface, инжектить через DI.
+----------------+-------------------------------+--------------------------+
| Конструкция | Связывание | Когда использовать |
+----------------+-------------------------------+--------------------------+
| self:: | Раннее (класс, где написано) | Точно знаем класс, |
| | | не ждём подмены |
| static:: | Позднее (LSB, класс вызова) | Наследуемые статические |
| | | методы, фабрики |
| parent:: | Родительский класс | Вызов реализации parent |
| | | при переопределении |
| $this-> | Текущий объект | Обычные методы |
| ClassName:: | Явно указанный класс | Из внешнего кода |
+----------------+-------------------------------+--------------------------+
self:: — когда вы пишете код, который не будет переопределяться в наследниках. Гарантия что вызывается именно этот класс.static:: — в фабриках (new static()) и методах, которые наследники могут переопределить. Это Late Static Binding.parent:: — чтобы дополнить (а не заменить) реализацию родителя; типично parent::__construct().ClassName::CONSTANT — константы класса всегда через ::, даже если у вас есть объект.Избегайте static-сервисов с состоянием — они не мокаются в тестах и текут под Octane.
С PHP 8.0+
new static() можно типизировать как : static в return type — явный полиморфизм для фабрик.
Видимость: public, protected, private
class User {
// public - доступно отовсюду (из класса, подклассов, и снаружи)
public $name;
// protected - доступно в классе и подклассах, но НЕ снаружи
protected $email;
// private - доступно ТОЛЬКО в этом классе
private $password;
public function __construct($name, $email, $password) {
$this->name = $name;
$this->email = $email;
$this->password = $password;
}
public function getEmail() {
return $this->email; // OK - внутри класса
}
protected function verifyPassword($pwd) {
return $this->password === $pwd; // OK
}
}
$user = new User("Alice", "alice@ex.com", "secret");
echo $user->name; // OK - public
echo $user->email; // ERROR - protected
echo $user->password; // ERROR - private
var_dumpvar_dump($user);
// Вывод:
// object(User)#1 (3) {
// ["name"]=> string(5) "Alice" ← public, обычное имя
// ["email":protected]=> string(11) "alice@ex.com" ← protected помечен
// ["password":"User":private]=> string(6) "secret" ← private + ИМЯ КЛАССА
// }
// Формат ключа в var_dump:
// "имя" — public
// "имя":protected — protected
// "имя":"ИмяКласса":private — private (привязан к конкретному классу!)
private в формате есть имя класса?
Потому что приватные свойства привязаны к конкретному классу, не к экземпляру. Если Admin extends User и у обоих есть private $id — это два разных свойства с одним именем. var_dump покажет оба: ["id":"User":private] и ["id":"Admin":private]. Поэтому имя класса в формате — обязательно.
(array)$user, получаешь те же ключи, но с null-байтами: "\0*\0email" (protected) и "\0User\0password" (private). Подробно — в разделе 1, подсекция «(array)$object с разной видимостью». var_dump показывает в человекочитаемой нотации (:protected, :"Class":private), а внутри объекта PHP хранит именно через null-байты.
Типизированные свойства (PHP 7.4+)
class User
{
private int $id; // без дефолта — должен быть инициализирован до доступа
private string $name = 'unknown'; // с дефолтом
private ?int $age = null; // nullable (?int = int|null)
private array $items = []; // массив, дефолт — пустой
private DateTime $createdAt; // объект
private readonly int $constId; // readonly (PHP 8.1+): записать можно ОДИН раз
}
class User
{
private int $id; // нет дефолта
}
$user = new User();
echo $user->id;
// Error: Typed property User::$id must not be accessed before initialization
// Без типа было бы просто null (warning), но с типом — fatal Error.
// Решение: задать дефолт ИЛИ инициализировать в конструкторе.
$x = 0, $arr = []), либо обязательно инициализируй в конструкторе. Иначе runtime ошибка при первом доступе. Это не баг — это явная семантика «свойство объявлено но не задано».
Naming conventions (PSR-12) — что писать с большой буквы
$user->AddItem() и $user->addItem() одинаково работают), но IDE / линтер / collega на code review подсветят несоответствие стандарту.
| Что | Стиль | Пример |
|---|---|---|
| Класс / Интерфейс / Trait / Enum | PascalCase | User, OrderRepository, PaymentInterface, HasFactory |
| Метод / функция | camelCase | addItem(), getTotal(), findByEmail() |
| Свойство / переменная | camelCase | $items, $totalAmount, $createdAt |
| Константа класса | UPPER_SNAKE | MAX_ITEMS, STATUS_ACTIVE |
| Файл с классом | PascalCase (как класс) | User.php, OrderRepository.php |
| Namespace | PascalCase по сегментам | App\Models, Illuminate\Support |
| Маршрут / URL | kebab-case | /user-profile, /order-details/{id} |
| База: таблица / колонка | snake_case | users, created_at, email_verified_at |
// Метод в PascalCase — путают с классом
public function AddItem($item) {}
// Метод в camelCase
public function addItem($item) {}
// Метод множественного числа когда добавляет ОДНО
public function addItems($item) {}
// Соответствие имени и действия
public function addItem($item) {} // один
public function addItems(array $items) {} // несколько
// Класс в lowercase
class user {}
// Класс в PascalCase
class User {}
echo — языковая конструкция, не функция
echo в PHP — это языковая конструкция (как if, return, break), а не функция. Поэтому со скобками вокруг аргумента она ведёт себя необычно — и это источник тонких багов.
echo "Hello"; // без скобок
echo "a", "b", "c"; // несколько аргументов через запятую
echo $user->summary(); // цепочка методов — БЕЗ скобок
echo($x)->method() работает «случайно»// Это часто пишут думая что echo — функция:
echo($user)->summary();
// PHP парсит это как:
// echo ( ($user)->summary() );
// ↑ echo принимает ОДНО выражение
// ($user) — это просто группировка
// ->summary() — вызов метода на User объекте
// результат: строка → echo выводит
// Работает СЛУЧАЙНО. Но если попробовать несколько аргументов:
echo($a, $b);
// Parse error! Скобки заставляют PHP интерпретировать как функцию,
// которая не принимает несколько аргументов через запятую.
// Без скобок — работает:
echo $a, $b; // верно
echo и print| Что | echo | print |
|---|---|---|
| Тип | Языковая конструкция | Языковая конструкция |
| Возвращает | Ничего (void) | Всегда 1 (можно использовать в выражении) |
| Несколько аргументов | echo $a, $b, $c | только один |
| Скорость | Чуть быстрее | Чуть медленнее (из-за return) |
echo без скобок. Для вывода объекта через метод — echo $obj->method();. Скобки вокруг echo создают иллюзию функции и могут сломать код через год (например, при добавлении второго аргумента).
Глоссарий: Инкапсуляция, Инвариант, DDD, Aggregate Root
| Термин | Уровень | Определение | Пример |
|---|---|---|---|
| private / protected / readonly | Инструмент языка | Модификаторы видимости PHP, технически запрещающие доступ извне. | private array $items = [] |
| Encapsulation (инкапсуляция) | Принцип ООП | Объект сам управляет своим состоянием. Внутренние данные скрыты, доступ только через методы. private — один из способов, не единственный. |
Контролируемый addItem($x) вместо прямой записи в массив |
| Invariant (инвариант) | Бизнес-правило | Условие, которое всегда должно соблюдаться у объекта (до операции, во время, после). Защищается инкапсуляцией. | «Корзина не может содержать > 50 товаров» |
| Aggregate Root | Паттерн DDD | Корневой класс группы связанных сущностей. Единственная точка входа для изменений всего агрегата. | Order владеет OrderItem[]; менять OrderItem можно только через Order |
| DDD (Domain-Driven Design) | Методология | Подход к проектированию сложных систем, где модель отражает бизнес-домен, а не структуру БД. Включает паттерны: Entity, Value Object, Aggregate, Repository, Domain Service. | Класс Order с методами place(), cancel() вместо CRUD-таблиц |
private?»
Нет. private — это популярный инструмент для реализации инкапсуляции в PHP, но не сама инкапсуляция.
- В Python нет «настоящего»
private, но инкапсуляция реализуется через соглашение_underscoreиpropertyдекораторы. - В Go нет ключевого слова
private— используется регистр имени (lowercase = пакетная видимость). - В PHP можно использовать
readonly(PHP 8.1+) — свойство публично читается, но запись запрещена. Тоже инкапсуляция.
Правильно сказать: «private — это удобный инструмент защиты состояния, а инкапсуляция — это ответственность класса самому управлять своим состоянием». Без понимания инвариантов private бесполезен.
private есть, инкапсуляция слабаяclass Cart
{
private array $items = [];
// Геттер возвращает ССЫЛКУ на массив — внешний код может менять
public function &getItems(): array
{
return $this->items;
}
}
$cart = new Cart();
$items = &$cart->getItems();
$items[] = 'обход проверок!';
// $items в Cart изменён напрямую, addItem() обойдён.
// private есть, но инвариант не защищён → инкапсуляция СЛАБАЯ.
final class Order
{
private array $items = [];
// ─── Команды защищают инварианты ───
public function addItem(Item $item): void
{
// Инвариант 1: не больше 100 товаров
if (count($this->items) >= 100) {
throw new DomainException('Лимит товаров');
}
// Инвариант 2: товар должен быть в каталоге
if (!$item->isAvailable()) {
throw new DomainException('Товар недоступен');
}
$this->items[] = $item;
}
// ─── Query возвращает COPY, не reference ───
public function items(): array
{
return $this->items; // PHP передаст копию (CoW для массивов)
}
}
- 1. Encapsulation — состояние private, нет обхода через геттеры по ссылке.
- 2. Invariants — проверки внутри команд явно защищают бизнес-правила.
- 3. Transactional boundary — один
save()атомарно сохраняет весь агрегат (Order + все OrderItem за раз).
Cross-ref: подробнее про DDD, паттерны Entity / Value Object / Repository — KB_5 Architecture → DDD basics.
Aggregate Root — паттерн «инкапсулированное состояние»
addItem(), removeItem()), это паттерн Aggregate Root из DDD. Класс владеет коллекцией и защищает её инварианты.
final class ShoppingCart
{
private array $items = []; // ← ИНКАПСУЛИРОВАННОЕ состояние
// ─── Команды: МЕНЯЮТ состояние, возвращают void ───
public function addItem(Item $item): void
{
// инварианты проверяются ЗДЕСЬ — не дать положить дубль/невалидное
if (count($this->items) >= 50) {
throw new DomainException('Cart full');
}
$this->items[] = $item;
}
public function removeItem(int $index): void
{
unset($this->items[$index]);
$this->items = array_values($this->items); // переиндексация
}
// ─── Query: ЧИТАЮТ состояние, не меняют ───
public function total(): float
{
return array_sum(array_map(
fn(Item $i): float => $i->price(),
$this->items
));
}
public function isEmpty(): bool
{
return $this->items === [];
}
public function itemCount(): int
{
return count($this->items);
}
}
- State в private — никто снаружи не может написать
$cart->items[] = $badв обход проверок. - Commands vs Queries разделены (CQS): команды меняют состояние и возвращают
void, queries только читают и возвращают данные. - Инварианты защищены — проверки внутри команд (
count < max, валидация и т.п.), не дают создать «невалидное» состояние.
На уровне middle+ часто спрашивают «как ты защищаешь инварианты» — этот паттерн и есть ответ. См. также KB_5 Architecture → DDD basics.
total()// ─── Подход 1: Cached / Materialized — хранить и обновлять ───
final class CartCached
{
private array $items = [];
private float $total = 0.0; // ← храним сумму
public function addItem(Item $item): void
{
$this->items[] = $item;
$this->total += $item->price(); // синхронизируем
}
public function removeItem(int $i): void
{
$this->total -= $this->items[$i]->price(); // НЕ ЗАБЫТЬ
unset($this->items[$i]);
}
public function total(): float
{
return $this->total; // O(1) — мгновенно
}
}
// ─── Подход 2: Computed / Derived — считать каждый раз ───
final class CartComputed
{
private array $items = [];
public function addItem(Item $item): void
{
$this->items[] = $item;
}
public function total(): float
{
// O(n) — проходим по items каждый вызов
return array_sum(array_map(
fn(Item $i): float => $i->price(),
$this->items
));
}
}
| Аспект | Cached (хранить) | Computed (считать) |
|---|---|---|
| Чтение total() | O(1) | O(n) — пройти весь массив |
| Запись (add/remove) | O(1) + обновить total | O(1) |
| Риск рассинхрона | ВЫСОКИЙ — добавили метод updatePrice() и забыли пересчитать total → баг | Нет — total всегда актуален |
| Сложность кода | Выше — синхронизация в каждом mutator | Ниже — одно место расчёта |
| Тестируемость | Хуже — скрытое state | Лучше — pure function |
| Sserialization | В JSON попадёт «лишний» total | Только items, total на лету |
- Начинай с Computed — проще, безопаснее, баги типа «забыл обновить» невозможны.
- Перепиши на Cached только когда профайлер показал что
total()вызывается тысячи раз и это узкое место. - Гибрид (lazy cache): поле
private ?float $cachedTotal = null. Вtotal()если null — считаем и кэшируем. ВaddItem/removeItemсбрасываем$cachedTotal = null. Получаем O(1) на повторных чтениях + автоматическую инвалидацию.
Анти-паттерн: $total = $items $item++ — синтаксически невалидно. PHP не имеет «магической связки» массив+оператор. Каждая операция явная: $this->items[] = $item; и (опционально) $this->total += $item->price();.
Constructor Promotion (PHP 8)
// Старый способ (PHP 7)
class User {
private $id;
private $name;
private $email;
public function __construct(int $id, string $name, string $email) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
}
// Новый способ (PHP 8 - Constructor Promotion)
class User {
public function __construct(
private int $id,
private string $name,
private string $email
) {}
}
// Автоматически создает private свойства и присваивает их!
$user = new User(1, "Alice", "alice@ex.com");
Static методы и свойства
class Counter {
public static $count = 0;
public static function increment() {
self::$count++;
}
public static function getCount() {
return self::$count;
}
}
// Вызывается через :: оператор, БЕЗ new
Counter::increment();
Counter::increment();
echo Counter::getCount(); // 2
// Практический пример - Factory method в Laravel
class User extends Model {
public static function findById(int $id) {
return static::find($id);
}
}
// Вызов
$user = User::findById(1);
Late Static Binding: static:: vs self::
class BaseModel {
public static $table = 'base';
public static function getTable() {
return self::$table; // ВСЕГДА вернет 'base'
}
public static function getTableLate() {
return static::$table; // Полиморфно! Вернет значение вызывающего класса
}
}
class User extends BaseModel {
public static $table = 'users';
}
class Post extends BaseModel {
public static $table = 'posts';
}
User::getTable(); // 'base' (self:: всегда указывает на BaseModel)
User::getTableLate(); // 'users' (static:: указывает на User)
Post::getTableLate(); // 'posts' (static:: указывает на Post)
final и readonly (PHP 8.1+)
class BaseEntity {
// final метод НЕ МОЖЕТ быть переопределен подклассом
final public function getId() {
return $this->id;
}
}
class User extends BaseEntity {
public function getId() { // ERROR!
}
}
// readonly свойства могут быть установлены ТОЛЬКО в конструкторе
class Product {
public readonly string $sku;
public readonly float $price;
public function __construct(string $sku, float $price) {
$this->sku = $sku;
$this->price = $price;
}
}
$product = new Product("SKU123", 99.99);
$product->price = 50; // ERROR! readonly свойство
final— запрещает переопределение в подклассе (для метода или класса целиком);readonly— запрещает изменение значения свойства после первой записи (обычно в конструкторе).
+-------------+--------------------+------------------------------+
| Ключ. слово | К чему применяется | Что запрещает |
+-------------+--------------------+------------------------------+
| final | методы, классы | переопределение метода |
| | | или наследование класса |
| readonly | свойства | изменение свойства |
| | | после инициализации |
+-------------+--------------------+------------------------------+
final НЕ влияет на свойства (нельзя сделать значения "неизменяемыми").
readonly НЕ влияет на методы (нельзя сделать "нельзя переопределить").
final class Money { // весь класс final
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
}
class SpecialMoney extends Money { }
// Fatal error: Class SpecialMoney cannot extend final class Money
// Зачем final class:
// — Value Object (Money, Email, Date) — гарантия что никто не наследует и не сломает инвариант
// — Service classes в DDD/Hexagonal — явная декларация что класс закрыт для расширения
// — Защита от хрупких иерархий: лучше явный отказ от наследования, чем глубокое дерево классов
class Base {
public readonly int $id;
public function __construct(int $id) {
$this->id = $id;
}
}
class Child extends Base {
public int $id; // переобъявили БЕЗ readonly
public function __construct(int $id) {
parent::__construct($id);
$this->id = 999; // теперь меняется, наследник убрал readonly
}
}
// PHP 8.4+ ужесточил это правило: readonly свойство в дочернем классе
// обязательно должно остаться readonly. Полный обход через переобъявление
// в большинстве версий работает, но это запах архитектуры.
// Чтобы гарантированно защитить — комбинируйте:
final class UserId { // final class — нельзя наследовать вообще
public function __construct(
public readonly int $value, // readonly — нельзя изменить
) {}
}
// Никто не сможет ни унаследовать, ни обойти readonly через переобъявление.
// Помечает ВСЕ свойства класса как readonly одной декларацией:
readonly class Order {
public function __construct(
public int $id, // автоматически readonly
public string $status, // автоматически readonly
public int $totalMinor, // автоматически readonly
) {}
}
// Эквивалентно:
// class Order {
// public function __construct(
// public readonly int $id,
// public readonly string $status,
// public readonly int $totalMinor,
// ) {}
// }
// Удобно для immutable DTO. Часто комбинируют с final:
final readonly class OrderData { ... }
— Хочешь, чтобы метод нельзя было переопределить →
final public function ...— Хочешь, чтобы весь класс нельзя было наследовать →
final class ...— Хочешь, чтобы значение свойства нельзя было изменить →
public readonly type $prop— Хочешь, чтобы ВСЕ свойства были readonly (PHP 8.2+) →
readonly class ...— Хочешь immutable DTO без наследования →
final readonly class ...Они не взаимозаменяемы.
readonly не запрещает override метода, final не запрещает изменение свойства.
Наследование и полиморфизм
- Расширение — делаем то же, что родитель, плюс что-то своё (через
parent::method()); - Замена — полностью переписываем логику под нужды наследника;
- Специализация — общий алгоритм подстраивается под конкретный случай;
- Полиморфизм — код, работающий с родительским типом, автоматически получает поведение наследника.
class Animal {
public function makeSound(): string {
return "Какой-то звук";
}
}
class Dog extends Animal {
public function makeSound(): string {
return "Гав!"; // замена реализации родителя
}
}
class Cat extends Animal {
public function makeSound(): string {
return "Мяу!";
}
}
$animals = [new Dog(), new Cat(), new Animal()];
foreach ($animals as $animal) {
echo $animal->makeSound() . "\n";
}
// Гав!
// Мяу!
// Какой-то звук
// Это и есть полиморфизм: foreach работает с типом Animal,
// но каждый объект "звучит" по-своему. Без override — все три
// вывели бы "Какой-то звук" (поведение родителя).
class User {
public function save(): void {
// базовое сохранение в БД
DB::insert('users', ['name' => $this->name]);
}
}
class AuditedUser extends User {
public function save(): void {
parent::save(); // сначала обычное сохранение
AuditLog::record('user.saved', $this->id); // потом аудит
}
}
// Логика родителя сохранена + добавлено своё.
// Если бы написали без parent::save() — забыли бы сохранение в БД.
class Controller {
protected function authorize(string $ability): void {
abort(403); // базовая логика: всегда запрещаем
}
}
class PostController extends Controller {
protected function authorize(string $ability): void {
// специализация: админам можно update
if ($ability === 'update' && auth()->user()->isAdmin()) {
return;
}
parent::authorize($ability); // остальное — по правилам родителя
}
}
// Если автор базового класса уверен, что метод НЕ должен меняться
// в наследниках — помечает его final:
class Database {
final public function connect(): void {
// жёсткая последовательность подключения, менять опасно
// (порядок: SSL handshake → auth → set names → setup)
}
}
class MysqlDatabase extends Database {
public function connect(): void { } // Fatal: Cannot override final method
}
// final — способ сказать: "Этот алгоритм критичен, не трогай."
interface PaymentGateway {
public function charge(int $amount): string;
}
class StripeGateway implements PaymentGateway {
public function charge(int $amount): string { /* Stripe API */ }
}
class PaddleGateway implements PaymentGateway {
public function charge(int $amount): string { /* Paddle API */ }
}
class CheckoutService {
public function __construct(private PaymentGateway $gateway) {}
public function processOrder(Order $order): string {
return $this->gateway->charge($order->total);
}
}
// CheckoutService не знает, Stripe это или Paddle.
// Он работает с КОНТРАКТОМ PaymentGateway, а конкретная реализация
// (полиморфно) определяется тем, что инжектировано через DI.
// Сменили Stripe на Paddle — CheckoutService не трогается ВООБЩЕ.
// Это основа SOLID/DI/тестируемого кода.
Когда поведение наследника логически отличается (Dog::makeSound vs Animal::makeSound).
Когда нужно добавить логику до/после родительской (parent::save + audit).
Когда реализуете интерфейс/абстрактный метод — обязаны переопределить.
Для специализации (admin-bypass в authorize).
Когда переопределять — нет:
Если метод критичный для контракта (тогда автор пометит
final).Просто чтобы «было своё» без реальной разницы поведения.
Когда лучше композиция: вместо
extends — инжектировать зависимость с другой реализацией интерфейса (Strategy pattern, см. KB_5).
class Database {
final public function connect(): void { ... }
}
class MysqlDatabase extends Database {
public function connect(): void { } // Fatal error: Cannot override final method
}
// final — явное запрещение от автора базового класса.
final class Money { ... }
class SpecialMoney extends Money { }
// Fatal: Class SpecialMoney cannot extend final class Money
// Невозможно даже создать подкласс — значит и override невозможен.
// Часто применяется для Value Object: Money, Email, UserId.
class Base {
private function hidden(): string { return "base"; }
public function callHidden(): string {
return $this->hidden(); // всегда вызовет Base::hidden(), не Child!
}
}
class Child extends Base {
private function hidden(): string { return "child"; }
// Синтаксически разрешено, но это ОТДЕЛЬНЫЙ метод в Child.
// Base::callHidden() о нём не знает.
}
echo (new Child())->callHidden(); // "base" — не "child"!
// Это частая ловушка: private методы НЕ наследуются по-настоящему.
// Они "приватны для класса", где объявлены. Подкласс может только
// СОЗДАТЬ свой private-метод с тем же именем — но это не override.
// Решение: используйте protected, если хотите чтобы наследники могли подменить.
// Даже если метод не final и не private, PHP проверяет совместимость сигнатуры.
// Правила (часть LSP — Liskov Substitution Principle):
// — visibility можно РАСШИРИТЬ (protected → public), но НЕ СУЗИТЬ
// — параметры: contravariant (тот же или шире/менее строгий тип)
// — return type: covariant (тот же или уже/более строгий тип)
// — нельзя поменять static/non-static
class Animal {
public function eat(Food $f): Food { ... }
}
class Dog extends Animal {
// Сужение видимости — Fatal Error
protected function eat(Food $f): Food { ... }
// Несовместимый тип параметра (более узкий, не contravariant)
public function eat(DogFood $f): Food { ... }
// Меняем static на не-static
public static function eat(Food $f): Food { ... }
// Уточнение return type (covariant) — разрешено
public function eat(Food $f): DogFood { ... }
// Расширение visibility — разрешено
public function eat(Food $f): Food { ... } // если в Animal был protected
}
class A {
public static function test(): string { return "A"; }
}
class B extends A {
public static function test(): string { return "B"; } // разрешено
}
B::test(); // "B"
// Технически разрешено. НО это не классический полиморфизм — нет $this,
// связывание ранее (через self::) идёт к классу, где метод написан.
// Для полиморфизма со static-методами используйте static:: (Late Static Binding,
// см. предыдущую подсекцию про self/static).
// А вот final static — переопределить нельзя:
class A {
final public static function test(): string { return "A"; }
}
class B extends A {
public static function test(): string { return "B"; } // Fatal
}
class User { }
class Admin extends User {
public function banUser(): void { ... } // в User такого метода нет — это новый метод, не override
}
// PHP не выдаёт ошибки, но семантически — это extension, не overriding.
// Иногда в IDE/линтерах настраивают атрибут #[\Override] (PHP 8.3+):
class Cat extends Animal {
#[\Override]
public function makeSound(): string { return "Мяу"; } // если в Animal есть makeSound
#[\Override]
public function makesSound(): string { ... } опечатка!
// PHP кинет Fatal — метода makesSound нет в родителе.
// Атрибут #[\Override] защищает от случайных опечаток и от ситуаций,
// когда родитель переименовал метод, а наследник остался со старым именем.
}
+-----------------------------------+----------------------+--------------------+
| Условие | Можно ли override? | Что произойдёт |
+-----------------------------------+----------------------+--------------------+
| Метод final в родителе | нет | Fatal Error |
| Класс final | нельзя наследовать | Fatal Error |
| Метод private | синтаксически да, | НЕ override — |
| | но это не override | новый метод в |
| | | дочернем |
| Сужение visibility | нет | Fatal Error |
| Несовместимый тип параметра | нет | Fatal Error |
| Несовместимый return type | нет (без covariance)| Fatal Error |
| Меняем static/non-static | нет | Fatal Error |
| Метод protected/public, не final | да | работает |
| static (не final) | да, но не полиморф | работает |
| Метода нет в родителе | да, но это новый, | предупреждение |
| | не override | если #[\Override] |
+-----------------------------------+----------------------+--------------------+
protected или public, не помечен final, и сигнатура совместима по LSP (visibility не сужается, параметры contravariant, return type covariant). Используйте #[\Override] (PHP 8.3+) на методах-наследниках, чтобы PHP проверял что override реально происходит — это спасёт от опечаток и refactor-разногласий.
class Animal {
protected $name;
public function __construct(string $name) {
$this->name = $name;
}
public function sound() {
return "Some sound";
}
public function introduce() {
return "I am " . $this->name;
}
}
class Dog extends Animal {
public function sound() {
return "Woof!"; // Переопределение
}
}
class Cat extends Animal {
public function sound() {
return "Meow!"; // Переопределение
}
}
// Полиморфизм - один интерфейс, разные поведения
makeSound(new Dog("Rex")); // "Woof!"
makeSound(new Cat("Whiskers")); // "Meow!"
function makeSound(Animal $animal) {
echo $animal->sound();
}
// Вызов родительского метода через parent::
class Dog extends Animal {
public function introduce() {
return parent::introduce() . " and I'm a dog!";
}
}
5. ООП: Абстрактные классы vs Интерфейсы
Абстрактные классы
abstract class Repository {
protected $db;
// Конкретный метод - имеет реализацию
public function setDb($db) {
$this->db = $db;
}
// Абстрактный метод - должен быть реализован подклассом
abstract public function findById(int $id);
// Абстрактный метод
abstract public function getAll();
}
class UserRepository extends Repository {
public function findById(int $id) {
return $this->$db->query("SELECT * FROM users WHERE id = ?", [$id]);
}
public function getAll() {
return $this->$db->query("SELECT * FROM users");
}
}
$repo = new Repository(); // ERROR! Cannot instantiate abstract class
$repo = new UserRepository(); // OK
- Объявляется ключевым словом
abstractпередclass; - Может содержать абстрактные методы — только сигнатура без тела:
abstract public function foo();(заканчивается точкой с запятой, без{}); - Любой наследник обязан реализовать все абстрактные методы (или сам стать абстрактным);
- Может содержать обычные методы с реализацией — они переиспользуются всеми наследниками;
- Может иметь свойства, конструктор, константы — всё как в обычном классе;
- Нельзя создать объект через
new— только через подкласс.
abstract class Animal {
protected string $name;
public function __construct(string $name) {
$this->name = $name;
}
// Готовый метод — все наследники получают бесплатно
public function getName(): string {
return $this->name;
}
// Абстрактный — каждый наследник обязан реализовать по-своему
abstract public function makeSound(): string;
}
class Dog extends Animal {
public function makeSound(): string { return "Гав!"; }
}
class Cat extends Animal {
public function makeSound(): string { return "Мяу!"; }
}
// $animal = new Animal("Test"); // Fatal: Cannot instantiate abstract class
$dog = new Dog("Бобик");
echo $dog->getName(); // "Бобик" — унаследовано
echo $dog->makeSound(); // "Гав!" — собственная реализация
// Каркас алгоритма фиксирован в родителе; шаги — в наследниках
abstract class CrudController extends Controller {
// Наследники обязаны указать модель и правила
abstract protected function modelClass(): string;
abstract protected function validationRules(Request $r): array;
// Общая логика для всех CRUD-контроллеров — пишется один раз
public function store(Request $request): JsonResponse {
$validated = $request->validate($this->validationRules($request));
$item = ($this->modelClass())::create($validated);
return response()->json($item, 201);
}
public function index(): JsonResponse {
return response()->json(($this->modelClass())::all());
}
}
class UserController extends CrudController {
protected function modelClass(): string { return User::class; }
protected function validationRules(Request $r): array {
return ['name' => 'required|string', 'email' => 'required|email|unique:users'];
}
}
class ProductController extends CrudController {
protected function modelClass(): string { return Product::class; }
protected function validationRules(Request $r): array {
return ['title' => 'required|string|max:100', 'price' => 'required|numeric|min:0'];
}
}
// Зачем абстрактный: store() и index() пишутся ОДИН раз в CrudController.
// Каждый наследник лишь указывает модель и правила. Если завтра добавится
// логирование запросов — добавляется в одном месте, работает для всех.
abstract class NotificationService {
public function __construct(
protected string $recipient,
protected string $message,
) {}
// Каркас: каждый наследник делает свой send()
abstract public function send(): bool;
// Общая утилита — все наследники переиспользуют
protected function log(string $status): void {
Log::info("[$status] Sent to $this->recipient");
}
}
class EmailService extends NotificationService {
public function send(): bool {
$ok = Mail::to($this->recipient)->send(new GenericMail($this->message));
$this->log($ok ? 'OK' : 'FAIL');
return $ok;
}
}
class SmsService extends NotificationService {
public function send(): bool {
$ok = Http::post('https://sms.api/send', [...])->successful();
$this->log($ok ? 'OK' : 'FAIL');
return $ok;
}
}
// Полиморфное использование — код не знает, какой именно сервис
$notifications = [
new EmailService('a@ex.com', 'Hi'),
new SmsService('+1234', 'Hi'),
];
foreach ($notifications as $n) {
$n->send(); // каждый отрабатывает по-своему, логирование общее
}
use Illuminate\Database\Eloquent\Model;
abstract class BaseEntity extends Model {
// Наследник указывает по какому полю искать (slug, username, etc.)
abstract public function searchField(): string;
// Универсальный поиск — работает для всех наследников
public static function findByUnique(string $value): ?static {
$instance = new static();
return static::where($instance->searchField(), $value)->first();
}
// Общий аксессор для формата даты — у всех моделей
public function createdAtShort(): string {
return $this->created_at->format('d.m.Y');
}
}
class Article extends BaseEntity {
public function searchField(): string { return 'slug'; }
}
class User extends BaseEntity {
public function searchField(): string { return 'username'; }
}
// Полиморфно — Article ищется по slug, User по username
$article = Article::findByUnique('hello-world');
$user = User::findByUnique('john_doe');
// findByUnique() написан 1 раз; каждый наследник без копипасты получает поиск.
// PHP в SPL даёт абстрактный FilterIterator — пишешь свой class extends...
// и обязательно реализуешь accept().
abstract class FilterIterator extends IteratorIterator {
// Этот метод сам PHP вызывает на каждой итерации,
// и наследник ОБЯЗАН его реализовать
abstract public function accept(): bool;
}
// Свой фильтр — оставляет только строки длиннее N символов
class MinLengthFilter extends FilterIterator {
public function __construct(Iterator $it, private int $min) {
parent::__construct($it);
}
public function accept(): bool {
return strlen((string) $this->current()) >= $this->min;
}
}
$words = new ArrayIterator(['a', 'hi', 'apple', 'no', 'banana']);
$filtered = new MinLengthFilter($words, 3);
foreach ($filtered as $w) echo $w . "\n";
// apple
// banana
// (короткие отфильтрованы PHP-движком, мы дали только правило)
- Есть общая логика для группы классов (поля, конструктор, готовые методы), которую глупо дублировать;
- Часть деталей нужно обязательно реализовать в каждом наследнике (абстрактные методы как контракт);
- Семантически «нет смысла создавать сам родитель» —
Animalвообще,NotificationServiceвообще,Repositoryвообще не существуют как объекты; - Нужно реализовать паттерн Template Method — общий алгоритм с переопределяемыми шагами.
- Если общей реализации нет, есть только контракт → interface;
- Если нужно реализовать несколько контрактов → интерфейсы (PHP не имеет множественного наследования классов);
- Если родитель сам по себе осмыслен и может существовать как объект → обычный класс + наследование без
abstract.
+-------------------------+--------+--------+----------+
| Характеристика | Class | Abstr. | Interface|
+-------------------------+--------+--------+----------+
| new ClassName() | | | |
| Готовые методы | | | (PHP<8.1)|
| Абстрактные методы | | | (по сути)|
| Свойства/поля | | | |
| Конструктор | | | |
| Множественное наследов. | 1 родит| 1 родит| много |
| Константы | | | |
| Тип в type hint | | | |
+-------------------------+--------+--------+----------+
— Если хочется дать каркас с готовой реализацией части методов + обязать наследников доделать остальное →
abstract class.— Если нужен только контракт (без общего кода) →
interface.— Если нужна и реализация, и множественное наследование поведения →
trait (см. KB_9).Абстрактный класс — полуфабрикат: даёт заготовку и правила, но требует доделки. Хорошо работает в связке с Strategy/Template Method патернами (см. KB_5).
Интерфейсы (Interfaces)
interface Loggable {
public function log(string $message);
}
interface Cacheable {
public function cache(string $key, $value);
public function getCache(string $key);
}
// Класс может имплементировать несколько интерфейсов
class UserService implements Loggable, Cacheable {
private $cache = [];
public function log(string $message) {
echo "LOG: " . $message;
}
public function cache(string $key, $value) {
$this->$cache[$key] = $value;
}
public function getCache(string $key) {
return $this->$cache[$key] ?? null;
}
}
$service = new UserService();
$service->log("User created");
$service->cache("user:1", "John");
Когда использовать Абстрактные классы vs Интерфейсы
| Абстрактный класс | Интерфейс |
|---|---|
| Общая функциональность для подклассов | Контракт для несвязанных классов |
| Может иметь свойства и приватные методы | Только публичные методы (PHP 8.1+) |
| Один класс может наследовать только один | Класс может имплементировать много |
| "IS-A" отношение (Dog IS-A Animal) | "CAN-DO" отношение (User CAN-DO Loggable) |
| Иерархия классов (специализация) | Набор способностей (миксины функциональности) |
// Абстрактный класс для иерархии: Animal -> Dog -> GoldenRetriever
abstract class Animal {}
class Dog extends Animal {}
class GoldenRetriever extends Dog {}
// Интерфейсы для способностей
interface Trainable {}
interface Loveable {}
// Dog может быть Trainable И Loveable
class Dog extends Animal implements Trainable, Loveable {
public function train() {}
public function love() {}
}
Default Methods в интерфейсах (PHP 8.1+)
interface Repository {
public function findById(int $id);
// Default реализация метода в интерфейсе
public function count() {
return count($this->getAll());
}
}
class UserRepository implements Repository {
public function findById(int $id) {
// реализация
}
// count() унаследуется с default реализацией
}
6. ООП: Traits
Trait vs Abstract Class vs Interface — сравнение
| Характеристика | Abstract Class | Interface | Trait |
|---|---|---|---|
| Концепция | «Является чем-то» (is-a) | «Умеет что-то» (can-do) | «Копипаст методов» (горизонталь) |
| Множественное использование | Один родитель | Сколько угодно | Сколько угодно |
| Свойства (поля) | Да, любая видимость | Только константы | Да, любая видимость |
| Реализация методов | Смесь abstract + concrete | Только сигнатуры | Полные методы |
| Конструктор | Да | Нет | Технически да, но опасно |
| Можно ли создать инстанс | Нет (только наследник) | Нет | Нет (только через use) |
| Приоритет при конфликте | Класс переопределяет родителя | — | Класс > Trait > Parent class |
| Полиморфизм через type-hint | Да | Да (рекомендовано) | Нет (trait — не тип) |
// ABSTRACT CLASS — общий родитель с базовой логикой и данными
// Используй когда: есть иерархия "is-a" и общее состояние
abstract class Animal {
protected string $name; // общее состояние
public function eat() { /* общее поведение */ }
abstract public function sound(): string; // подкласс ОБЯЗАН
}
// INTERFACE — контракт способностей, без реализации
// Используй когда: нужно описать capability для type-hint
interface Trainable {
public function learn(string $command): void;
}
// TRAIT — переиспользуемое поведение для классов разных веток
// Используй когда: один и тот же метод нужен в неродственных классах
trait Loggable {
public function log(string $msg): void {
error_log(static::class . ": $msg");
}
}
// КОМБИНАЦИЯ — реальный код часто использует все три
class Dog extends Animal implements Trainable {
use Loggable;
public function sound(): string { return "Woof"; }
public function learn(string $cmd): void { $this->log("Learned $cmd"); }
}
- Abstract — когда есть "is-a" иерархия и общее состояние (BaseController, BaseModel)
- Interface — когда нужен контракт для DI / полиморфизма (PaymentGatewayInterface)
- Trait — когда один и тот же код нужен в неродственных классах (Loggable, Cacheable, Notifiable)
Основы Traits
trait Timestamp {
public $createdAt;
public $updatedAt;
public function setTimestamps() {
$this->createdAt = new DateTime();
$this->updatedAt = new DateTime();
}
}
trait SoftDelete {
public $deletedAt;
public function softDelete() {
$this->deletedAt = new DateTime();
}
public function restore() {
$this->deletedAt = null;
}
}
class Post {
use Timestamp, SoftDelete;
private $title;
public function __construct(string $title) {
$this->title = $title;
$this->setTimestamps(); // From Timestamp trait
}
}
$post = new Post("Hello");
$post->softDelete(); // From SoftDelete trait
Разрешение конфликтов: insteadof и as
trait A {
public function handle() {
echo "Trait A";
}
}
trait B {
public function handle() {
echo "Trait B";
}
}
class MyClass {
use A, B {
// insteadof - выбрать один метод, другой игнорировать
B::handle insteadof A;
// as - создать алиас для метода A (переименовать)
A::handle as handleA;
}
}
$obj = new MyClass();
$obj->handle(); // "Trait B"
$obj->handleA(); // "Trait A"
Абстрактные методы в Traits
trait Validatable {
// Абстрактный метод - подклассы ДОЛЖНЫ реализовать
abstract public function rules();
// Конкретный метод, который использует абстрактный
public function validate($data) {
$rules = $this->rules();
// Валидировать $data против $rules
}
}
class UserForm {
use Validatable;
public function rules() {
return [
'email' => 'required|email',
'password' => 'required|min:8'
];
}
}
Реальные use cases Traits
// 1. Логирование для разных классов
trait Loggable {
public function log($msg) {
echo date('Y-m-d H:i:s') . ": " . $msg;
}
}
class UserService { use Loggable; }
class OrderService { use Loggable; }
// 2. Кэширование для модели
trait Cacheable {
private $cache = [];
public function remember($key, callable $callback) {
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
return $this->cache[$key] = $callback();
}
}
// 3. JSON сериализация
trait JsonSerializable {
public function toJson() {
return json_encode($this->toArray());
}
abstract public function toArray();
}
Пояснялка — частые вопросы по разделу
① Что такое isset в trait Cacheable?
В фрагменте кода if (isset($this->cache[$key])) — это встроенная языковая конструкция PHP (не часть trait). Она проверяет, что элемент массива $this->cache с ключом $key существует И не равен null. В контексте трейта Cacheable логика такая: если ключ есть → вернуть закэшированное значение (без повторного вычисления); если нет → вызвать $callback(), сохранить результат в $cache[$key] и вернуть.
② Почему именно isset, а не array_key_exists?
isset($this->cache[$key]) вернёт true, только если элемент существует и его значение не null. Если кэшируемая функция может вернуть null, то isset ошибочно решит, что кэша нет, и вызовет $callback() повторно. В таких случаях лучше array_key_exists($key, $this->cache) — он проверяет только наличие ключа. Автор трейта, вероятно, исходит из того, что $callback() никогда не возвращает null, либо такое поведение допустимо.
③ Метод validate() из trait — обязательно реализовывать в классе?
Нет. validate() в trait уже имеет готовую реализацию — она «вмешивается» в класс через use Validatable. Класс UserForm получает её бесплатно и может вызвать $form->validate($data) без переопределения. Обязателен только rules() — у него abstract модификатор. Trait декларирует: «я умею валидировать, но правила задаёт класс-пользователь». Если класс не реализует rules() — fatal error:
Fatal error: Class UserForm contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Validatable::rules)
Возможные сценарии для класса с trait:
- Abstract метод trait (
rules()) — обязан реализовать класс. - Concrete метод trait (
validate()) — не обязан, наследует as-is. Может переопределить, если хочет другую логику. - Свойство trait (
$cache) — попадает в класс автоматически.
Это та же логика, что у abstract class: abstract method = контракт, concrete method = бесплатное наследство.
④ Интерфейсы могут хранить свойства?
Нет — интерфейс не может содержать свойства (поля). Только сигнатуры методов (и, в некоторых случаях, константы). Он говорит: «Класс, который меня реализует, обязан иметь такие-то методы», но как эти методы будут работать и какие внутренние свойства для этого понадобятся — решает сам класс.
interface CacheableInterface {
public function remember($key, callable $callback);
}
// Класс сам решает, хранить ли кэш в свойстве $cache, в БД, в Redis и т.д.
class UserService implements CacheableInterface {
private $cache = []; // ← свойство придумал класс, интерфейс его не требует
public function remember($key, callable $callback) { /* ... */ }
}
⑤ Трейты могут хранить свойства? Можно ли их изменить в классе?
Да — трейт может определять свойства (поля) с любым модификатором (public, protected, private). Когда класс подключает трейт через use, все эти свойства становятся частью класса, как будто они были написаны прямо в классе.
Что значит «класс может изменить их»?
- Изменить значения свойств — да, легко. Класс может перезаписать
$this->cache = []в своём методе. - Изменить само объявление (видимость
privateнаprotected, тип или значение по умолчанию) — так просто не выйдет. Если класс повторно объявит свойство с тем же именем, PHP выдаст ошибку, если объявления несовместимы. Можно переопределить метод, который работает со свойством, или использовать механизмinsteadof / as— но для свойств это редко и сложно.
Правильнее сказать: трейт даёт классу готовые свойства, а класс может использовать и модифицировать их значения, но не может «отменить» или «изменить сигнатуру» самого свойства без конфликта.
⑥ Ограничения trait Cacheable
- Свойство
private $cache— область видимостиprivateвнутри трейта, но оно становится частью класса, использующего трейт. Если в классе уже есть свойство$cache— конфликт (его можно разрешить в классе). - Кэш живёт только в пределах одного запроса (in-memory, не сохраняется между HTTP-запросами).
- Для долговременного кэширования (Redis, memcached) — этот трейт не подходит. Используй
Cache::remember().
7. Магические методы
__construct и __destruct
class Database {
private $connection;
// __construct вызывается при создании объекта
public function __construct(string $host, string $user) {
$this->connection = mysqli_connect($host, $user);
echo "Connected";
}
// __destruct вызывается при удалении объекта
public function __destruct() {
mysqli_close($this->connection);
echo "Disconnected";
}
}
$db = new Database("localhost", "root");
// Output: "Connected"
$db = null; // или выход из области видимости
// Output: "Disconnected"
$host, $user, $connection, что такое RAII, когда деструктор не подходит — см. «Вопросник / Объяснялка» в конце раздела.
// 1. FileHandler — открыть/закрыть файл автоматически
class FileHandler {
private $handle;
private string $path;
public function __construct(string $path, string $mode = 'r') {
$this->path = $path;
$this->handle = fopen($path, $mode);
if (!$this->handle) throw new Exception("Cannot open file");
}
public function read(): string {
return fread($this->handle, filesize($this->path));
}
public function __destruct() {
if ($this->handle) fclose($this->handle); // гарантированно закроется
}
}
// 2. FileLock — захват/освобождение файловой блокировки
class FileLock {
private $handle;
public function __construct(string $file) {
$this->handle = fopen($file, 'c');
flock($this->handle, LOCK_EX); // эксклюзивная блокировка
}
public function __destruct() {
flock($this->handle, LOCK_UN);
fclose($this->handle);
}
}
// 3. Logger — открыть лог, дописать стартовую/финальную запись
class Logger {
private $logFile;
public function __construct(string $filename) {
$this->logFile = fopen($filename, 'a');
$this->write("Logger started");
}
public function write(string $message): void {
fwrite($this->logFile, date('Y-m-d H:i:s') . " - $message\n");
}
public function __destruct() {
$this->write("Logger finished");
fclose($this->logFile);
}
}
// 4. TransactionGuard — гарантированный rollback при сбое
class TransactionGuard {
public function __construct(private PDO $db) {
$this->db->beginTransaction();
}
public function commit(): void { $this->db->commit(); }
public function __destruct() {
// если commit не был вызван — откатываем
if ($this->db->inTransaction()) $this->db->rollBack();
}
}
// Использование:
function transfer(PDO $db): void {
$tx = new TransactionGuard($db);
// ... любой throw здесь автоматически откатит транзакцию через __destruct
$tx->commit();
}
__get, __set, __isset, __unset
class User {
private $data = [];
// Вызывается при доступе к приватному/несуществующему свойству
public function __get(string $name) {
echo "Getting $name";
return $this->data[$name] ?? null;
}
// Вызывается при установке приватного/несуществующего свойства
public function __set(string $name, $value) {
echo "Setting $name to $value";
$this->data[$name] = $value;
}
// Вызывается при isset() на приватном свойстве
public function __isset(string $name) {
return isset($this->data[$name]);
}
// Вызывается при unset() на приватном свойстве
public function __unset(string $name) {
unset($this->data[$name]); // удалить ключ из внутреннего массива
}
}
$user = new User();
$user->name = "Alice"; // Вызовет __set
echo $user->name; // Вызовет __get
isset($user->name); // Вызовет __isset
unset() — см. «Вопросник / Объяснялка» в конце раздела.
$user = new User();
$user->name = "Alice";
// PHP: нет свойства $name → __set('name', 'Alice') → $data['name'] = 'Alice'
echo $user->name;
// PHP: нет свойства $name → __get('name') → return $data['name'] = 'Alice'
var_dump(isset($user->name));
// PHP: нет свойства $name → __isset('name') → isset($data['name']) = true
unset($user->name);
// PHP: нет свойства $name → __unset('name') → unset($data['name'])
var_dump(isset($user->name));
// PHP: → __isset('name') → false (ключ удалён из $data)
$attributes, и обращение $user->name идёт через __get, который читает из $attributes['name'] и применяет accessor/cast.
__call и __callStatic
class Builder {
private $query = [];
// __call - вызывается для приватных/несуществующих методов
public function __call(string $name, $args) {
$this->query[$name] = $args[0] ?? null;
return $this; // Fluent interface
}
// __callStatic - статический вызов несуществующего метода
public static function __callStatic(string $name, $args) {
return new self()->__call($name, $args);
}
public function get() {
return $this->query;
}
}
// Создается dynamic методы: select, where, orderBy
$builder = new Builder();
$builder->select('*')->where('id = 1')->orderBy('name');
print_r($builder->get());
// ['select' => '*', 'where' => 'id = 1', 'orderBy' => 'name']
__toString, __invoke, __clone
class Price {
public function __construct(float $amount) {
$this->amount = $amount;
}
// __toString - преобразование объекта в строку
public function __toString() {
return sprintf('$%.2f', $this->amount);
}
// __invoke - позволяет вызвать объект как функцию
public function __invoke(float $percent) {
return $this->amount * (1 + $percent / 100);
}
// __clone - вызывается при клонировании объекта
public function __clone() {
echo "Cloning price object";
}
}
$price = new Price(99.99);
echo $price; // "$99.99" (вызовет __toString)
echo $price(10); // 109.989 (вызовет __invoke с 10% скидка)
$copy = clone $price; // Вызовет __clone
__debugInfo и __serialize
class User {
private $id = 1;
private $password = 'secret';
public $name = 'Alice';
// __debugInfo - контролирует что показывается в var_dump/print_r
public function __debugInfo() {
return [
'id' => $this->id,
'name' => $this->name,
// password скрыт!
];
}
// __serialize - контролирует сериализацию (PHP 7.4+)
public function __serialize() {
return [
'id' => $this->id,
'name' => $this->name
// password НЕ сериализуется
];
}
// __unserialize - восстановление из сериализации
public function __unserialize($data) {
$this->id = $data['id'];
$this->name = $data['name'];
}
}
Пояснялка — частые вопросы по разделу
① Откуда в примере Database берутся $host, $user и $connection?
Распространённое замешательство: в коде класса нет видимого определения $host и $user. Они не являются полями класса — они приходят извне.
$host,$user— параметры конструктора. Не определены где-то заранее. Получают значения в момент создания объекта:$db = new Database("localhost", "root")— «localhost» становится$host, «root» становится$user.$connection— свойство класса (private $connection;). Внутри конструктора выполняется$this->connection = mysqli_connect($host, $user). Функцияmysqli_connect()открывает реальное TCP-соединение с сервером MySQL и аутентифицируется. Возвращает объект-ссылку на соединение, который сохраняется в свойстве.- Связь с БД:
mysqli_connect()внутри устанавливает TCP-соединение с MySQL по указанному хосту (localhost= сокет или127.0.0.1:3306) и аутентифицируется с указанным пользователем. Если пароль не передан, PHP попытается подключиться без пароля — сработает только если у пользователя действительно нет пароля. - В реальных проектах параметры обычно читаются из
.env/config.phpи передаются в конструктор. Это стандартный приём: класс не знает, какой именно хост и пользователь будут использоваться — это решает тот, кто создаёт объект. - Опечатка-капкан: в исходном коде было написано
$this->$connection(с двумя$). Правильно:$this->connection(без лишнего$после->).$this->$connection— это обращение к свойству, имя которого хранится в переменной$connection(variable property). Если переменная не определена — fatal error.
② Что значит «несуществующее свойство» для __get/__set/__isset/__unset?
Это свойство, которое явно не объявлено в классе (нет ни public $name, ни protected $name, ни private $name). В примере класса User есть только private $data — свойства $name у класса нет, но обращение $user->name = "Alice" работает благодаря __set.
Магические методы срабатывают в двух случаях:
- Свойство не объявлено вообще — как
$user->nameв примере. PHP не находит свойство → вызывает__set() / __get()и не выдаёт ошибку. - Свойство объявлено, но недоступно в текущем контексте — например,
private $name, а вы пытаетесь обратиться к нему извне класса. В этом случае__get / __setтоже будут вызваны.
Почему в примере используется private $data = []? Вместо реальных свойств объектов автор использует «виртуальные» свойства, которые хранятся внутри массива $data. Это позволяет:
- Динамически создавать любые поля без их предварительного объявления.
- Контролировать доступ через геттеры/сеттеры.
- Легко преобразовывать объект в массив или JSON (достаточно вернуть
$data).
Поток вызовов:
$user = new User();
$user->name = "Alice"; // нет свойства $name → __set('name', 'Alice')
echo $user->name; // нет свойства $name → __get('name') → 'Alice'
var_dump(isset($user->name));// __isset('name') → true
unset($user->name); // __unset('name')
var_dump(isset($user->name));// __isset('name') → false
Где применяется в реальном коде: прокси-объекты, ORM (например, Eloquent — поля модели соответствуют столбцам БД), реализация «ленивого» чтения свойств, декораторы и обёртки.
③ Что такое unset() внутри __unset?
unset() — это встроенная языковая конструкция PHP (не функция, как strlen()). Делает одно из трёх в зависимости от аргумента:
unset($var)— уничтожает переменную (помечает память для GC, но не мгновенно).unset($array[$key])— удаляет элемент массива по ключу. После этогоisset($array[$key])=falseиarray_key_exists()=false.unset($obj->prop)— удаляет свойство объекта (либо вызывает__unset, если свойства нет).
В нашем коде: unset($this->data[$name]) удаляет ключ $name из приватного массива $data. Это нужно, чтобы виртуальное свойство «исчезло» — в следующий раз isset($user->name) вернёт false.
Важный нюанс: для индексированных массивов ([1,2,3]) unset не переиндексирует числовые ключи. После unset($arr[1]) массив станет [0 => 1, 2 => 3] — пропуск в ключах. Чтобы переиндексировать, используй array_values($arr).
Разница для переменных: unset переменной уничтожает её, освобождая память. Для массива — удаляет элемент.
④ Что такое RAII? Зачем __construct + __destruct?
RAII (Resource Acquisition Is Initialization) — паттерн родом из C++: конструктор захватывает ресурс, деструктор освобождает его. Применим и в PHP, Python, Ruby. Позволяет избежать утечек и забытого освобождения ресурсов даже если код упал с exception.
Частые сценарии:
- Файлы:
FileHandler—fopenв конструкторе,fcloseв деструкторе. Гарантировано закроется даже еслиread()бросил исключение. - Сокеты:
SocketClient—fsockopen→fclose. Авто-закрытие сетевых соединений. - Блокировки файла / семафоры:
FileLock—flock LOCK_EX→flock LOCK_UN. Блокировка автоматически снимется при выходе из scope. - Транзакции БД:
TransactionGuard—beginTransactionв конструкторе, в деструктореif (inTransaction()) rollBack(). Защита от незакрытых транзакций при сбое. - GD-изображения / тяжёлые ресурсы:
ImageResource—imagecreatefromjpeg→imagedestroy. Освобождение памяти не дожидаясь GC. - Логирование:
Logger— открыть лог в конструкторе с записью «Logger started», в деструкторе — «Logger finished» иfclose.
⑤ Когда деструктор НЕ подходит?
- Завершение работы с БД — обычно не требует закрытия в деструкторе, т.к. соединение PDO закрывается автоматически при уничтожении объекта или в конце скрипта.
- Сложная логика очистки — деструктор вызывается при сборке мусора, порядок не гарантирован, могут быть зависимости на другие объекты, которые уже были уничтожены.
- Критические операции — нельзя полагаться на деструктор, чтобы сохранить данные или отправить подтверждение. Например, в long-running процессах (queue workers, daemons) лучше явно вызывать метод
close(). - PDO / Redis — обычно закрываются автоматически при уничтожении объекта, явный close в
__destructизбыточен.
⑥ Какова цена за магические методы?
Магические методы медленнее прямого доступа к свойству (lookup + вызов метода) и усложняют IDE-автодополнение. Их используют для прокси-объектов (например, ORM, где поля соответствуют столбцам БД), реализации «ленивого» чтения свойств, декораторов и обёрток. В большинстве простых классов лучше явно объявлять свойства и писать обычные геттеры/сеттеры (getName(), setName()) — код понятнее и быстрее.
8. Namespaces & PSR-4 Autoloading
Синтаксис Namespaces
// app/Models/User.php
namespace App\Models;
class User {
public $name = 'Alice';
}
// app/Services/UserService.php
namespace App\Services;
use App\Models\User; // Import
class UserService {
public function getUser() {
return new User(); // Используем импортированный User
}
}
// index.php или другой файл
use App\Services\UserService;
use App\Models\User as UserModel; // Alias
$service = new UserService();
PSR-4 Autoloading с Composer
{
"name": "myapp/core",
"autoload": {
"psr-4": {
"App\\": "app/",
"App\\Models\\": "app/models/",
"App\\Services\\": "app/services/",
"App\\Http\\Controllers\\": "app/http/controllers/",
"Tests\\": "tests/"
}
},
"require": {
"php": ">=8.0"
}
}
// Файл: app/Models/User.php
namespace App\Models; // Namespace соответствует пути
// Файл: app/Http/Controllers/UserController.php
namespace App\Http\Controllers; // Полный путь в namespace
Использование Namespaces
// Полное имя (fully qualified)
$user = new App\Models\User();
// С use - более читаемо
use App\Models\User;
$user = new User();
// Alias (as)
use App\Models\User as UserModel;
$user = new UserModel();
// Множественные import
use App\Models\User;
use App\Models\Post;
use App\Services\UserService;
// Или с группировкой (PHP 7.1+)
use App\Models\{User, Post};
use App\Services\{UserService, PostService as PostHandler};
// Относительное использование (для классов в текущей namespace)
namespace App\Models;
class User {
public function getPosts() {
return new Post(); // App\Models\Post автоматически
}
}
Автоматическая загрузка классов
// После composer install, подключи автолоадер
require 'vendor/autoload.php'; // Composer автолоадер
// Теперь можешь использовать любой класс из конфигурации
use App\Models\User;
use App\Services\UserService;
$user = new User(); // Автолоадер загружает app/Models/User.php
$service = new UserService(); // Загружает app/Services/UserService.php
9. Обработка ошибок
Разбор throw new InvalidArgumentException(...) построчно
throw new InvalidArgumentException('Numeric expected');
// ↑ ↑ ↑ ↑
// 1 2 3 4
// 1. throw — оператор "выбросить исключение"
// Останавливает нормальное выполнение и передаёт управление
// ближайшему catch-блоку выше по стеку. Если catch нет —
// необработанное исключение → fatal error, скрипт умирает.
// 2. new — оператор создания объекта (см. раздел 4 "ООП")
// Создаёт ЭКЗЕМПЛЯР указанного класса. throw принимает
// только объекты-исключения, не классы и не строки.
// 3. InvalidArgumentException — встроенный класс PHP, потомок Exception.
// По соглашению выбрасывается когда аргумент функции/метода
// имеет неправильный тип или значение. Сигнализирует
// "программист передал плохие данные".
// 4. 'Numeric expected' — аргумент конструктора (сообщение об ошибке).
// Произвольная строка, доступна потом через $e->getMessage().
// Можно дополнительно передать код ошибки и предыдущее исключение:
// new InvalidArgumentException($msg, $code, $previousException)
function calculate($amount, $quantity): float
{
if (!is_numeric($amount) || !is_numeric($quantity)) {
// ← throw остановит функцию, никакого return уже не будет
throw new InvalidArgumentException('Numeric expected');
}
return (float)$amount * (float)$quantity;
}
try {
// Код который МОЖЕТ выбросить исключение
echo calculate('abc', 5); // бросит исключение
// — следующая строка НЕ выполнится
echo "Этот код не выполнится";
} catch (InvalidArgumentException $e) {
// Сюда попадаем если throw сработал.
// $e — пойманный объект исключения, у него методы:
// $e->getMessage() — наше "Numeric expected"
// $e->getFile() — файл где брошено
// $e->getLine() — строка
// $e->getTrace() — стек вызовов
// $e->getCode() — код ошибки (опц.)
echo "Ошибка: " . $e->getMessage();
} finally {
// finally выполнится ВСЕГДА:
// — после try (если исключения не было)
// — после catch (если было обработано)
// — даже если в catch новый throw (перед его дальнейшим распространением)
// Используется для очистки ресурсов (закрыть файл, отпустить блокировку).
echo "Финал";
}
return false или echo "error"?
- Невозможно проигнорировать —
return falseвызывающий может забыть проверить. Исключение либо ловят, либо весь скрипт падает. - Разделение нормального и аварийного пути — основной поток функции остаётся чистым (
return $result), исключения уходят вcatchотдельно. - Контекст ошибки — кроме сообщения автоматически сохраняется стек вызовов: где, в каком файле, в какой строке, кто вызвал.
- Иерархия — можно ловить по типу —
catch (InvalidArgumentException)отдельно отcatch (DatabaseException). Разная реакция на разные ошибки.
| Класс | Когда выбрасывать | Пример |
|---|---|---|
InvalidArgumentException | Плохой аргумент — тип или значение | В calculate() пришла строка вместо числа |
OutOfRangeException | Индекс вне границ | Доступ к $arr[100] в массиве из 10 |
LengthException | Неверная длина строки/массива | Пароль короче 8 символов |
RuntimeException | Сбой во время выполнения (не баг) | API не отвечает, БД недоступна |
LogicException | Программная ошибка (баг) | Метод вызван в неправильном порядке |
DomainException | Значение вне допустимого набора | Передан статус 'unknown', ожидались active/paused |
UnexpectedValueException | Полученное значение не соответствует ожиданиям | API вернул JSON вместо XML |
- throw — для аварийных ситуаций, которые не часть нормального выполнения.
- return false / null — для нормального «не нашлось»:
User::find($id)при отсутствии возвращаетnull, это не повод для throw. - Имя класса исключения = тип проблемы. Часто спрашивают разницу
InvalidArgumentExceptionvsRuntimeException— первое значит «программист передал плохие данные», второе значит «среда подвела (БД упала)». - Не лови
Exceptionогульно — лови конкретный тип. Иначе скроешь баги, которые надо чинить.
Exception Hierarchy
| Тип | Описание | Примеры |
|---|---|---|
| Throwable | Базовый интерфейс для всех | - |
| Exception | Обычные исключения (код ошибка) | InvalidArgumentException |
| Error | Ошибки PHP (не всегда ловятся) | TypeError, DivisionByZeroError |
| ParseError | Синтаксическая ошибка | eval() с плохим синтаксисом |
try {
$file = fopen('/nonexistent/file.txt', 'r');
if (!$file) {
throw new Exception('File not found');
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
} finally {
// Выполняется ВСЕГДА, даже если есть исключение
if (isset($file)) {
fclose($file);
}
}
Множественные catch блоки
try {
$user = getUserById(1);
$age = processAge($user['age']);
} catch (UserNotFoundException $e) {
echo "User not found";
log($e);
} catch (InvalidAgeException $e) {
echo "Invalid age";
} catch (Exception $e) {
// Ловим все остальные Exception-ы
echo "Something went wrong";
} catch (Throwable $e) {
// Ловим и Exception и Error
echo "Critical error";
}
Пользовательские исключения
class InvalidEmailException extends Exception {
public function __construct(string $email) {
$message = "Invalid email: $email";
parent::__construct($message);
}
}
class ValidationException extends Exception {
public function __construct(array $errors) {
$this->errors = $errors;
parent::__construct(json_encode($errors));
}
public function getErrors() {
return $this->errors;
}
}
function validateEmail(string $email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException($email);
}
}
try {
validateEmail("invalid");
} catch (InvalidEmailException $e) {
echo $e->getMessage();
}
Error Reporting и set_exception_handler
// Установить уровень ошибок
error_reporting(E_ALL); // Все ошибки
ini_set('display_errors', 1); // Показывать ошибки (development)
ini_set('log_errors', 1); // Логировать ошибки
ini_set('error_log', '/var/log/php-errors.log');
// Глобальный обработчик исключений
set_exception_handler(function($e) {
echo "Exception caught: " . $e->getMessage();
log_error($e);
http_response_code(500);
});
// Глобальный обработчик ошибок
set_error_handler(function($errno, $errstr, $errfile, $errline) {
if ($errno === E_USER_ERROR) {
throw new Exception($errstr);
}
return false; // Позволить стандартному обработчику работать
});
// Вызови пользовательскую ошибку
trigger_error("Something bad", E_USER_WARNING);
10. PHP 8.x Новые фичи
Named Arguments
function createUser(string $name, string $email, int $age, bool $active = true) {
return ['name' => $name, 'email' => $email, 'age' => $age, 'active' => $active];
}
// Позиционные аргументы (старый способ)
createUser("Alice", "alice@ex.com", 30, false);
// Именованные аргументы (PHP 8)
createUser(
name: "Alice",
email: "alice@ex.com",
age: 30,
active: false
);
// Можно менять порядок!
createUser(
email: "alice@ex.com",
age: 30,
name: "Alice"
);
// Смешивать позиционные и именованные
createUser("Alice", "alice@ex.com", age: 30);
Match Expression
// Старый способ (switch)
$status = "pending";
switch ($status) {
case "pending":
$label = "Pending Review";
break;
case "approved":
$label = "Approved";
break;
default:
$label = "Unknown";
}
// Новый способ (match - PHP 8)
$label = match($status) {
"pending" => "Pending Review",
"approved" => "Approved",
"rejected" => "Rejected",
default => "Unknown"
};
// Match с несколькими условиями
$message = match($statusCode) {
200, 201 => "Success",
400, 401, 403 => "Client error",
500, 502, 503 => "Server error",
default => "Unknown"
};
// Match обычно приносит условие
$price = match(true) {
$quantity > 100 => 50,
$quantity > 50 => 75,
default => 100
};
Enums (PHP 8.1+)
// Pure Enum (без значений)
enum Status {
case PENDING;
case APPROVED;
case REJECTED;
}
// Backed Enum (с значениями)
enum HttpStatus: int {
case OK = 200;
case CREATED = 201;
case BAD_REQUEST = 400;
case NOT_FOUND = 404;
case SERVER_ERROR = 500;
}
// Enum с методами
enum Role: string {
case ADMIN = 'admin';
case USER = 'user';
case GUEST = 'guest';
public function label(): string {
return match($this) {
self::ADMIN => 'Administrator',
self::USER => 'Regular User',
self::GUEST => 'Guest User',
};
}
}
// Использование
$role = Role::ADMIN;
echo $role->value; // "admin"
echo $role->label(); // "Administrator"
// Получить enum по значению
$status = HttpStatus::tryFrom(404); // HttpStatus::NOT_FOUND
Nullsafe Operator
// Старый способ (много проверок)
$name = null;
if ($user !== null && $user->profile !== null) {
$name = $user->profile->name;
}
// Новый способ (PHP 8 nullsafe)
$name = $user?->profile?->name;
// Если $user null, весь результат null
echo $name ?? "No name";
// С методами
$result = $user?->profile?->getName();
First-class Callable Syntax (PHP 8.1+)
class Math {
public static function add($a, $b) {
return $a + $b;
}
}
// Старый способ
$adder = [Math::class, 'add'];
echo $adder(5, 3);
// Новый способ (PHP 8.1)
$adder = Math::add(...); // Синтаксис ...() делает функцию объектом
echo $adder(5, 3);
// С методами
$user = new User();
$getName = $user->getName(...);
echo $getName();
// Передать в функцию высшего порядка
$numbers = [1, 2, 3];
$result = array_map(intval(...), $numbers);
Readonly Properties и Classes (PHP 8.1+)
class ImmutableUser {
public readonly int $id;
public readonly string $email;
public function __construct(int $id, string $email) {
$this->id = $id;
$this->email = $email;
}
}
$user = new ImmutableUser(1, "test@ex.com");
$user->email = "new@ex.com"; // ERROR! readonly свойство
// Readonly класс (PHP 8.2) - все свойства readonly
readonly class Point {
public function __construct(
public int $x,
public int $y
) {}
}
11. Генераторы (Generators)
yield ключевое слово
// Обычная функция (загружает все в память)
function getNumbers($start, $end) {
$result = [];
for ($i = $start; $i <= $end; $i++) {
$result[] = $i * $i;
}
return $result;
}
$numbers = getNumbers(1, 1000000); // Создает массив из 1M элементов в памяти!
// Генератор (ленивое вычисление, по одному элементу за раз)
function yieldNumbers($start, $end) {
for ($i = $start; $i <= $end; $i++) {
yield $i * $i; // Возвращает значение, сохраняет состояние
}
}
// Использование генератора
foreach (yieldNumbers(1, 1000000) as $number) {
echo $number . "\n"; // Обрабатывает по одному элементу, минимум памяти
}
Ключи и yield с массивами
// yield с ключами (для ассоциативных данных)
function readCsvFile(string $file) {
$fp = fopen($file, 'r');
$header = fgetcsv($fp);
$line = 1;
while (($row = fgetcsv($fp)) !== false) {
$data = array_combine($header, $row);
yield $line => $data; // yield с ключом
$line++;
}
fclose($fp);
}
// Использование - читает файл построчно
foreach (readCsvFile('data.csv') as $lineNum => $row) {
echo "Line $lineNum: " . $row['email'];
}
yield from (делегирование)
function innerGenerator() {
yield 1;
yield 2;
yield 3;
}
function outerGenerator() {
yield 0;
yield from innerGenerator(); // Делегирует всем значениям
yield 4;
}
foreach (outerGenerator() as $value) {
echo $value; // 0 1 2 3 4
}
Практический пример: Пагинация
// Получить пользователей в батчах (для пагинации в БД)
function getUsersInBatches(int $batchSize = 100) {
$page = 1;
while (true) {
$users = User::paginate($batchSize, $page);
if ($users->isEmpty()) {
break;
}
foreach ($users as $user) {
yield $user;
}
$page++;
}
}
// Обрабатывать всех пользователей БЕЗ загрузки всех в памяти
foreach (getUsersInBatches(1000) as $user) {
$user->sendNotification();
}
12. Closures и Anonymous Functions
Базовые Closures
// Анонимная функция (Closure)
$greeting = function($name) {
return "Hello, $name!";
};
echo $greeting("Alice"); // "Hello, Alice!"
// Передать closure как параметр
function processUsers(array $users, callable $callback) {
foreach ($users as $user) {
$callback($user);
}
}
$users = [
['name' => 'Alice'],
['name' => 'Bob']
];
processUsers($users, function($user) {
echo "Processing " . $user['name'];
});
use Keyword - Захват переменных
$multiplier = 3;
// Closure по умолчанию НЕ имеет доступа к переменным снаружи
$multiply = function($x) {
return $x * $multiplier; // ERROR! $multiplier не доступна
};
// use - захватить переменную из внешней области
$multiply = function($x) use ($multiplier) {
return $x * $multiplier;
};
echo $multiply(5); // 15
// use по ссылке (&) - захватить переменную и позволить изменять
$counter = 0;
$increment = function() use (&$counter) {
$counter++;
};
$increment();
$increment();
echo $counter; // 2
// Несколько переменных в use
$tax = 0.1;
$discount = 0.05;
$calculatePrice = function($price) use ($tax, $discount) {
return $price * (1 + $tax) * (1 - $discount);
};
Arrow Functions (PHP 7.4+)
// Старый способ (анонимная функция)
$numbers = [1, 2, 3];
$squared = array_map(function($n) {
return $n * $n;
}, $numbers);
// Новый способ (arrow function)
$squared = array_map(fn($n) => $n * $n, $numbers);
// Arrow functions автоматически захватывают переменные!
$multiplier = 5;
$multiply = fn($x) => $x * $multiplier; // $multiplier захвачена автоматически
echo $multiply(3); // 15
// Arrow functions - один выражение, автоматически возвращает
$getFullName = fn($user) => $user['first'] . " " . $user['last'];
// С несколькими параметрами
$sum = fn($a, $b) => $a + $b;
echo $sum(5, 3); // 8
Binding и $this в Closures
class Calculator {
private $value = 10;
public function getAdder() {
// Closure может использовать $this если определена в методе класса
return function($x) {
return $this->value + $x; // Доступ к $this
};
}
}
$calc = new Calculator();
$adder = $calc->getAdder();
echo $adder(5); // 15
// bindTo - привязать closure к другому объекту
class Price {
private $amount = 100;
}
$getClosure = function() {
return $this->amount;
};
$price = new Price();
$bound = $getClosure->bindTo($price, Price::class);
echo $bound(); // 100
Практический пример: Middleware
// Middleware как closures
$request = ['method' => 'POST', 'path' => '/api/users'];
$response = ['status' => 200];
// Middleware для логирования
$logger = function($next) {
return function($req) use ($next) {
echo "Request: " . $req['method'] . " " . $req['path'];
$response = $next($req);
echo "Response: " . $response['status'];
return $response;
};
};
// Middleware для авторизации
$auth = function($next) {
return function($req) use ($next) {
if (!isset($req['token'])) {
return ['status' => 401];
}
return $next($req);
};
};
// Основной обработчик
$handler = function($req) {
return ['status' => 200, 'body' => 'Success'];
};
// Compose middleware
$pipeline = $logger($auth($handler));
$result = $pipeline($request);
Шпаргалка PHP — всё в одной таблице
Типы данных и операторы сравнения
| Конструкция | Как работает | Пример |
|---|---|---|
== (loose) | Type juggling: приводит типы перед сравнением | '0' == false → true |
=== (strict) | Сравнение типа И значения | '1' === 1 → false |
<=> (spaceship) | Возвращает -1 / 0 / 1 | 1 <=> 2 → -1 |
?? (null coalescing) | Значение если null/undefined | $a ?? 'default' |
?-> (nullsafe) | Цепочка вызовов без NPE (PHP 8) | $user?->profile?->avatar |
declare(strict_types=1) | Требует точный тип в аргументах | Кидает TypeError при несовпадении |
(array)object | Cast объекта в массив | private → "\0ClassName\0prop" (null-байты!) |
// 6 значений (все остальные — true):
false, 0, 0.0, "", "0", null, []
// Ловушки:
// "0.0" → true (это строка не пустая!)
// "false" → true (это строка не пустая!)
// [0] → true (массив не пустой)
Старое поведение (PHP < 8.0): при == между строкой и числом — строка приводилась к числу. Нечисловая строка → 0. Поэтому "abc" == 0 было true — источник классических уязвимостей.
Новое поведение (PHP 8.0+): если строка нечисловая — наоборот, число приводится к строке, сравниваются две строки. Если строка числовая ("10", "1e2") — по-прежнему сравниваются как числа.
| Выражение | PHP < 8.0 | PHP 8.0+ | Почему |
|---|---|---|---|
"10" == 10 | true | true | Строка числовая, сравнение как числа (не изменилось) |
"10" === 10 | false | false | Strict: string ≠ int (не изменилось) |
"" == 0 | true | false | Нечисловая (пустая) строка. PHP 8: 0 → "0", "" ≠ "0" |
"abc" == 0 | true | false | Нечисловая строка. PHP 8: 0 → "0", "abc" ≠ "0" |
"0" == 0 | true | true | Строка числовая (значение 0) |
"1abc" == 1 | true | false | «Полу-числовая» — раньше парсилась до 1, в PHP 8: nonnumeric → сравнение строк |
"100" == "1e2" | true | true | Обе строки числовые: 100 == 100 |
'0' == false | true | true | Сравнение с bool — другие правила (приведение к bool) |
- Строка ЧИСЛОВАЯ + число → сравнение как числа (
"10" == 10→ true) - Строка НЕчисловая + число → число приводится к строке (
"abc" == 0→ false) - Безопасное правило: в production-коде используй ТОЛЬКО
===. Формулировать так: «всегда===, изменение PHP 8 знаю наизусть».
Массивы — ключевые функции
| Функция | Что делает | Возвращает | Ключи |
|---|---|---|---|
array_map | Трансформация каждого элемента | Новый массив | Сбрасывает (числовые) или сохраняет (assoc) |
array_filter | Фильтрация по callback | Новый массив | Сохраняются! (часто баг) |
array_reduce | Свёртка в одно значение | Любой тип | — |
array_walk | Мутация in-place (по ссылке &) | bool | — |
array_merge | Сложение, числовые переиндексируются | Новый массив | String-ключи перезаписываются |
+ (объединение) | Левый имеет приоритет | Новый массив | Сохраняются (без переиндексации) |
usort | Сортировка с callback (spaceship) | bool (in-place) | Сбрасываются |
array_column | Вытащить столбец из массива объектов | Массив значений | — |
array_key_exists | Ключ есть (даже если value = null) | bool | — |
isset | Ключ есть И не null | bool | — |
| Флаг | Сигнатура callback | Когда применять |
|---|---|---|
| по умолчанию | fn($value) | Стандартная фильтрация по значению |
ARRAY_FILTER_USE_KEY | fn($key) | Фильтрация по ключу (префикс/суффикс ключа) |
ARRAY_FILTER_USE_BOTH | fn($value, $key) | Когда условие зависит от обоих |
fn($carry, $item) — 1-й аккумулятор, 2-й текущий элемент. Имена НЕ важны, важен порядок. Это зашито в реализацию PHP.
ООП — abstract vs interface vs trait
| Характеристика | Abstract Class | Interface | Trait |
|---|---|---|---|
| Множественное использование | один родитель | сколько угодно | сколько угодно |
| Свойства (поля) | любая видимость | только константы | любая видимость |
| Реализация методов | микс abstract+concrete | только сигнатуры | полные методы |
| Конструктор | да | нет | технически да, опасно |
| Создать инстанс | нет | нет | только через use |
| Type-hint в функциях | да | да (рекомендовано) | trait — не тип |
| Семантика | «is-a» | «can-do» | «копипаст методов» |
| Когда выбирать | иерархия + общее состояние | контракт для DI / полиморфизма | код для разных веток |
| Конструкция | Резолвится в | Использовать когда |
|---|---|---|
self:: | Класс где написано (compile-time) | Нужен именно ЭТОТ класс, без подмены |
static:: | Реальный вызываемый класс (runtime) | Полиморфизм через статику (фабрики, ORM) |
parent:: | Родительский класс | Вызов родителя из переопределённого метода |
Магические методы
| Метод | Когда вызывается | Типичный use case |
|---|---|---|
__construct | new Class(...) | Инициализация, RAII захват ресурса |
__destruct | $obj = null / выход из scope | RAII освобождение ресурса |
__get($name) | Чтение несуществующего/private | ORM атрибуты, прокси |
__set($name, $value) | Запись несуществующего/private | ORM атрибуты, валидация в сеттере |
__isset($name) | isset($obj->prop) на магическом | Поддержка isset для виртуальных свойств |
__unset($name) | unset($obj->prop) на магическом | Удаление виртуального свойства |
__call($name, $args) | Вызов несущ. метода | Fluent Builder, Query Builder |
__callStatic($n, $a) | Вызов несущ. static метода | Facade pattern (Laravel) |
__toString() | (string)$obj, echo $obj | Value Objects (Money → "$100") |
__invoke($args) | $obj() — объект как функция | Single Action Classes (Laravel) |
__clone() | clone $obj | Глубокое клонирование (deep copy) |
__debugInfo() | var_dump / print_r | Скрыть password, секреты из дампа |
__serialize() / __unserialize | serialize($obj) | Контроль сериализации, защита secrets |
PHP 8.x — ключевые фичи
| Фича | Версия | Пример | Зачем |
|---|---|---|---|
match | 8.0 | match($x) { 1, 2 => 'a' } | Строгое сравнение (vs switch), возвращает значение |
enum | 8.1 | enum Status { case Active; } | Типизированные константы, методы |
readonly | 8.1 | public readonly int $id; | Иммутабельность (Value Objects, DTO) |
| Constructor promotion | 8.0 | __construct(private int $id) | Меньше boilerplate в DTO |
| Named arguments | 8.0 | foo(name: 'Alice', age: 30) | Читаемость, можно пропускать опц. параметры |
?-> nullsafe | 8.0 | $user?->profile?->avatar | Цепочка без NullPointerException |
| First-class callable | 8.1 | strlen(...) | Передача функции как closure без обёртки |
| Attributes | 8.0 | #[Route('/path')] | Декларативная конфигурация (Symfony Routes) |
| Fibers | 8.1 | new Fiber(fn() => ...) | Корутины, основа Amphp/ReactPHP |
| Intersection types | 8.1 | Countable&Iterator $x | Множественные требования к типу |
Top-30 тем — что нужно знать
| # | Тема | Уровень | Раздел KB |
|---|---|---|---|
| 1 | == vs === (type juggling) | Junior+ | 1. Типы данных |
| 2 | False-y значения (что превращается в false) | Junior+ | 1. Типы данных |
| 3 | abstract vs interface vs trait | Middle | 5. ООП Abstract, 6. Traits |
| 4 | self:: vs static:: (Late Static Binding) | Middle | 4. ООП Основы |
| 5 | RAII: __construct + __destruct | Middle | 7. Магические методы |
| 6 | array_map vs filter vs reduce | Junior+ | 3. Массивы |
| 7 | & в array_walk — by reference | Middle | 3. Массивы |
| 8 | Lifecycle PHP-запроса (index.php → response) | Middle | cross-ref KB_3 |
| 9 | PSR-4 autoloading | Middle | 8. Namespaces |
| 10 | composer.json sections | Junior+ | 8. Namespaces |
| 11 | Exception hierarchy (Error vs Exception) | Middle | 9. Обработка ошибок |
| 12 | Generators yield — когда, зачем | Middle | 11. Генераторы |
| 13 | Closures: use() vs arrow fn | Middle | 12. Closures |
| 14 | Магические методы — цена за магию | Middle | 7. Магические методы |
| 15 | PDO prepared statements (защита от SQLi) | Junior+ | cross-ref KB_2/KB_4 |
| 16 | password_hash vs md5 | Junior+ | cross-ref KB_4 |
| 17 | Spread operator ... в массивах и аргументах | Middle | 3. Массивы |
| 18 | Union types int|string | Middle | 1. Типы данных |
| 19 | Nullable ?int | Junior+ | 1. Типы данных |
| 20 | Constructor promotion (PHP 8) | Middle | 10. PHP 8+ |
| 21 | match vs switch | Middle | 10. PHP 8+ |
| 22 | enum (PHP 8.1) | Middle | 10. PHP 8+ |
| 23 | readonly свойства | Middle | 4. ООП Основы |
| 24 | Attributes (PHP 8) — Laravel routes/validation | Senior | 10. PHP 8+ |
| 25 | Fibers — корутины | Senior | 10. PHP 8+ |
| 26 | opcache / JIT | Senior | cross-ref KB_6 |
| 27 | Copy-on-Write для массивов | Senior | 3. Массивы |
| 28 | SplObjectStorage, WeakMap | Senior | — |
| 29 | Iterator vs Generator | Middle | 11. Генераторы |
| 30 | __invoke — Single Action Classes | Middle | 7. Магические методы, cross-ref KB_5 |
Вопросник для самопроверки (PHP Core)
Уровень 1 — Junior+ · 10 вопросов
== и ===? Приведи пример где результат отличается.== (loose) приводит типы перед сравнением — это type juggling. === (strict) сравнивает и тип, и значение.
Пример: '0' == false → true (PHP сначала привёл к bool, получил false, сравнил с false). Но '0' === false → false (тип строки и тип bool — разные).
PHP 8.0 изменил правила: сравнение нечисловой строки с числом раньше было "abc" == 0 → true (строка → 0). С PHP 8.0 наоборот — число приводится к строке, поэтому "abc" == 0 → false (т.к. "abc" ≠ "0"). Подробнее — в Шпаргалке, раздел «Типы».
Правило: всегда используй ===. Loose-сравнение даёт неочевидные баги.
var_dump('0' == false)? А var_dump('0' === false)?'0' == false → bool(true). Строка '0' в bool-контексте превращается в false, потому сравнение false == false = true.
'0' === false → bool(false). Тип string ≠ тип bool.
Ловушка: '0.0' == false → false (это непустая строка, не равна '0'). Поэтому filter_var в PHP осторожнее.
Type juggling — автоматическое приведение типов при сравнении или операциях. PHP пытается «угадать» что ты хотел.
Опасен в трёх местах: 1) сравнения паролей — $hash == $userInput может магически совпасть с 0; используй hash_equals(). 2) in_array($needle, $haystack) без 3-го параметра true — найдёт "foo" если в массиве есть 0. 3) ключи массивов — числовые строки "5" и числа 5 резолвятся в один ключ.
6 значений, которые в bool-контексте становятся false: false, 0, 0.0, "", "0", null, [] (пустой массив).
Ловушки: "0.0" → true (непустая строка!), "false" → true (строка), [0] → true (непустой массив).
isset и array_key_exists?isset($arr[$key]) — true если ключ есть И значение не null.
array_key_exists($key, $arr) — true если ключ есть, даже если значение null.
Когда применять array_key_exists: если null — валидное значение в твоём массиве (например, кэш может закэшировать null как результат).
declare(strict_types=1)?Включает строгую типизацию для скалярных type-hints в текущем файле. Должен быть первым оператором в файле.
Без него: function add(int $a, int $b) примет add('5', 3) — PHP приведёт строку к int. Со strict_types=1 — выкинет TypeError.
Формулировать так:: «использую strict_types=1 во всех файлах, это требование PSR-12 для production-кода».
fn (arrow) и function (anonymous)?Arrow function fn($x) => $x*2 — короткий синтаксис, автоматически захватывает переменные из родительского scope по значению. Только одно выражение, без {}.
Anonymous function function($x) use ($y) { ... } — нужен явный use для захвата. Может содержать любое тело. По умолчанию захват по значению (по ссылке — use (&$y)).
private от protected?private — доступно только в самом классе. Даже наследник не видит.
protected — доступно в классе и во всех наследниках.
На практике: используй private по умолчанию (инкапсуляция). protected — только когда осознанно проектируешь расширяемость через наследование.
$this? Когда нельзя использовать?$this — ссылка на текущий экземпляр объекта внутри метода класса.
Нельзя использовать: в static методах (нет инстанса — есть только класс); в обычных функциях вне класса; в __destruct после уничтожения других полей.
В static-методе вместо $this — self:: (текущий класс) или static:: (Late Static Binding).
echo "Hello $name" vs echo 'Hello $name'?Double quotes "..." — интерполируют переменные: echo "Hello $name" → "Hello Alice" (если $name = 'Alice').
Single quotes '...' — литералы, переменные НЕ интерполируются: echo 'Hello $name' → "Hello $name" буквально.
Также в double quotes работают escape-последовательности: "\n" = перенос строки, в single — буквально 2 символа.
Уровень 2 — Middle · 20 вопросов
Abstract class — частичная реализация. Может иметь свойства, конкретные методы, абстрактные методы. Один родитель.
Interface — только контракт (сигнатуры). Свойств нет, только константы. Можно реализовать сколько угодно.
Когда: abstract — если есть общее состояние и «is-a» иерархия (BaseController); interface — если нужен контракт для DI / полиморфизма (PaymentInterface). На практике часто комбинируют: class Dog extends Animal implements Trainable.
Trait — горизонтальное переиспользование кода. Класс может подключить сколько угодно трейтов через use. Решает проблему отсутствия множественного наследования.
Отличие от abstract: abstract — это «is-a» (Animal → Dog). Trait — это «has-behavior» (Loggable, Cacheable, Notifiable). Используй trait когда метод нужен в неродственных классах.
Abstract class: да, любые свойства любой видимости.
Interface: только константы (const), свойств нет.
Trait: да, любые свойства. При use попадают в класс как родные. Если в классе уже есть свойство с тем же именем — fatal error при несовместимом объявлении.
static:: отличается от self::?self:: — резолвится в класс, где написан код (compile-time). static:: — резолвится в реально вызванный класс (runtime).
Пример: class A { static function make() { return new static(); } }. Если B extends A, то B::make() с self:: вернёт A, а с static:: — B.
Применение: фабричные методы, Eloquent Model::create() возвращает экземпляр конкретной модели.
final? А readonly?final class — нельзя унаследоваться. final method — нельзя переопределить в наследниках.
readonly (PHP 8.1) — свойство можно проинициализировать ОДИН раз (в конструкторе), потом — fatal error при попытке записи. Применяется в Value Objects, DTO.
Разница: final запрещает переопределение, readonly запрещает изменение значения.
__construct — при new Class(...).
__destruct — при $obj = null или выходе из scope (RAII).
__get($name) — при чтении несуществующего/недоступного свойства (Eloquent attributes).
__call($name, $args) — при вызове несуществующего метода (fluent builders, facades).
__toString() — при (string)$obj или echo $obj (Value Objects, Money → "$100").
__construct/__destruct.RAII (Resource Acquisition Is Initialization) — паттерн из C++: конструктор захватывает ресурс, деструктор освобождает. Гарантирует освобождение даже при throw.
Примеры:
- FileHandler —
fopenв конструкторе,fcloseв деструкторе. - TransactionGuard —
beginTransaction→rollBackесли не былоcommit(). - FileLock —
flock LOCK_EX→LOCK_UN. Блокировка снимется при выходе из scope.
$user->name = 'X', если в классе нет свойства $name?PHP не находит свойство $name → проверяет, есть ли в классе __set().
Если __set есть → вызывается с аргументами ('name', 'X'). Без ошибки.
Если __set нет → создаётся динамическое публичное свойство $user->name = 'X'. В PHP 8.2+ — deprecated warning. В PHP 9 — будет fatal error без #[AllowDynamicProperties].
PSR-4 — стандарт autoloading. Маппит namespace на путь к файлу: App\Models\User → app/Models/User.php. Composer генерирует автозагрузчик.
Autoload избавляет от ручных require: PHP сам подгрузит файл при первом использовании класса. Без него — гора require в каждом файле.
Настройка в composer.json: "autoload": { "psr-4": { "App\\\\": "app/" } }.
require, require_once и autoload?require — подгружает файл, fatal error если файла нет. Каждый раз заново.
require_once — подгружает только если ещё не подгружен (отслеживается на уровне PHP).
Autoload — PHP сам вызывает функцию (зарегистрированную через spl_autoload_register) когда видит неизвестный класс. Composer регистрирует PSR-4 автозагрузчик.
В современном коде require/require_once почти не пишут — всё через Composer autoload.
array_reduce. Что такое аккумулятор?array_reduce($arr, $callback, $initial) — сворачивает массив в одно значение, последовательно применяя callback.
Аккумулятор — промежуточный результат. На каждой итерации callback получает ($accumulator, $currentItem) и возвращает новый аккумулятор.
Пример: сумма массива [1,2,3,4] с initial 0 → 0+1=1, 1+2=3, 3+3=6, 6+4=10. Возвращает 10.
PHP жёстко зашил порядок аргументов callback: 1-й — аккумулятор, 2-й — текущий элемент. Имена переменных не важны.
array_map, array_filter, array_walk?array_map($cb, $arr) — трансформирует, возвращает новый массив. Ключи сбрасываются для числовых массивов.
array_filter($arr, $cb) — фильтрует, возвращает новый массив. Ключи сохраняются (это частый сюрприз — после фильтрации могут быть «дырки» в индексах).
array_walk($arr, $cb) — мутирует массив in-place. Callback принимает &$value по ссылке. Возвращает bool.
& в array_walk(&$item)?& — передача по ссылке. Без неё callback получит копию значения и не сможет изменить элемент массива.
С & любые изменения $item внутри callback меняют сам массив (in-place mutation).
Контраст с array_map: map создаёт новый массив (без мутации), walk — мутирует исходный.
ARRAY_FILTER_USE_KEY?Флаг для array_filter — меняет что приходит в callback: вместо значения приходит ключ.
Без флага: array_filter($arr, fn($value) => ...).
С USE_KEY: array_filter($arr, fn($key) => str_ends_with($key, '_id'), ARRAY_FILTER_USE_KEY) — фильтрация по ключу.
Есть ещё ARRAY_FILTER_USE_BOTH — в callback приходят оба: fn($value, $key).
... в массивах? В аргументах?В аргументах: function sum(...$nums) — variadic, собирает аргументы в массив.
При вызове: sum(...[1,2,3]) — распаковывает массив в отдельные аргументы.
В массивах (PHP 7.4+): $merged = [...$arr1, ...$arr2, 'extra'] — расширение массивов. Быстрее чем array_merge.
С string-ключами (PHP 8.1+): [...$assoc1, ...$assoc2] — работает, дубликаты ключей перезаписываются справа.
yield)? Чем отличается от обычной функции?Generator — функция, которая «лениво» возвращает значения через yield вместо return. Не загружает весь массив в память.
Когда нужен: чтение больших файлов построчно (миллионы строк), обработка потоков, бесконечные последовательности.
Пример: чтение CSV — file_get_contents загружает 10 GB в память → OOM. Generator с fgets → 1 строка за раз, постоянное потребление памяти.
use ($var)? Можно ли менять переменную в closure?Closure — анонимная функция, которая «захватывает» переменные из внешнего scope.
use ($var) — явный список переменных для захвата. По умолчанию по значению (копия). Для захвата по ссылке: use (&$var).
Менять переменную: по значению — изменения внутри closure не видны снаружи. По ссылке — видны.
Arrow function (fn) автоматически захватывает всё нужное по значению, без use.
Error и Exception.Оба наследуют Throwable, оба ловятся через catch.
Exception — бизнес/доменные ошибки (validation, not found, unauthorized). Их ловят и обрабатывают.
Error — системные/программные (TypeError, ParseError, OutOfMemoryError). Обычно НЕ ловят — это баг, чинить код, а не глотать.
Иерархия: Throwable ← Error / Exception. Чтобы поймать всё: catch (\Throwable $t).
match? Чем отличается от switch?match (PHP 8) — выражение, возвращает значение. Использует строгое сравнение ===. Не нужен break. Если ни одна ветка не подошла — UnhandledMatchError.
switch — оператор (не выражение). Использует loose ==. Нужен break, иначе fall-through. Если ничего не подошло и нет default — молча идёт дальше.
В новом коде — match предпочтительнее: безопаснее, короче.
Enum — типизированный набор значений. Можно использовать как type-hint: function setStatus(Status $s).
Преимущества над const: 1) защита от случайных значений ('active' vs typo); 2) можно добавлять методы; 3) IDE подсказывает все варианты; 4) Status::cases() вернёт все значения.
Виды: Pure enum (без значений) и Backed enum (enum Status: string { case Active = 'active'; }) для сериализации в БД.
Уровень 3 — Middle+/Senior · 15 вопросов
Attributes — нативный механизм декларативной метаинформации над классами/методами/свойствами. До PHP 8 жили в doc-комментариях как аннотации.
Пример Symfony: #[Route('/users/{id}', methods: ['GET'])] public function show(int $id) {...}.
Laravel: #[Route] в роутинге PHP 8 style, #[Validate('email')] в формах.
Читаются через рефлексию (ReflectionClass::getAttributes()).
Fibers — корутины с явным управлением. Можно suspend в середине выполнения и resume снаружи.
Отличие от generators: generator может только yield вверх по стеку (своему caller). Fiber может suspend/resume произвольно — это основа для async-фреймворков (AmpHP, ReactPHP).
На практике: писать fibers напрямую почти не приходится — используют поверх async-библиотек.
opcache — кэширует скомпилированный bytecode PHP в shared memory. Без него каждый запрос парсит .php файлы заново.
JIT (Just-In-Time) — компиляция «горячего» bytecode в нативный машинный код. Включается в opcache (opcache.jit=tracing).
Эффект: на типичных веб-задачах (ввод/вывод) JIT даёт ~3-5%. На вычислительных (math, image processing) — до 2-3x.
WeakMap)? Зачем нужны?WeakReference / WeakMap — хранят объект, но не мешают GC его удалить. Если объект больше нигде не используется — он удаляется, ссылка обнуляется.
Зачем: кэш по объектам, observers/listeners (чтобы listener не удерживал объект в памяти бесконечно), object identity maps в ORM.
До PHP 7.4 пришлось бы хранить ID + использовать обычный массив — но это утечка памяти.
PHP использует reference counting + cycle collector. Каждый объект имеет счётчик ссылок (refcount). При refcount=0 — удаляется.
Circular reference — два объекта ссылаются друг на друга ($a->b = $b; $b->a = $a). Refcount никогда не станет 0. Cycle collector периодически их находит и удаляет.
В коде это можно увидеть через gc_collect_cycles(). В long-running процессах (queue workers) важно следить за memory leaks.
Когда массив присваивается другой переменной ($b = $a) — PHP не копирует данные сразу. Обе переменные ссылаются на одни данные с refcount=2.
Копия создаётся только при первой модификации одной из них ($b[] = 1). Это и есть Copy-on-Write.
Эффект: передача массива в функцию by-value — бесплатна, пока никто не пишет. Поэтому immutable-стиль в PHP не такой дорогой как кажется.
serialize объектов? Как её решает __serialize?serialize($obj) сериализует все свойства объекта, включая приватные. Проблема: secrets / passwords / API tokens попадают в сериализованный вид. Если такой объект сохраняется в кэш — leak.
__serialize() (PHP 7.4+) — позволяет вернуть array с тем, что хочешь сохранить. Полный контроль.
__unserialize($data) — восстанавливает объект из этого массива.
SplObjectStorage? Зачем нужен?SplObjectStorage — структура «массив, где ключ — объект, значение — любые данные».
В обычном массиве PHP ключ может быть только string/int — объект использовать нельзя. SplObjectStorage работает по spl_object_hash($obj).
Применение: object identity maps в ORM, observer registries, dependency graphs.
iterable через Iterator vs Generator?Iterator — интерфейс с 5 методами: current(), key(), next(), rewind(), valid(). Полный контроль над итерацией. Можно несколько раз пройти.
Generator — функция с yield. Короче, но одноразовый: после прохождения нельзя rewind (бросает исключение).
В Laravel LazyCollection — обёртка над Generator с возможностью «обернуть в Iterator» при необходимости.
Stringable interface (PHP 8)?Stringable — интерфейс с одним методом __toString(): string. Любой класс с __toString автоматически считается Stringable (это implicit interface).
Применение: type-hint function log(string|Stringable $msg) — принимает и строки, и объекты с __toString (например, Money, Url).
static переменных в функциях.function f() { static $count = 0; $count++; return $count; } — $count сохраняется между вызовами, инициализируется один раз.
Это не глобальная переменная — она видна только внутри функции. Полезно для счётчиков, мемоизации (хотя для серьёзной мемоизации лучше делать отдельный объект-кэш).
Не работает с recursion-эффектами в parallel testing — статика общая на процесс.
__invoke? Зачем делать класс callable?__invoke($args) — магический метод, позволяющий вызвать объект как функцию: $obj($arg1, $arg2).
Зачем: Single Action Classes в Laravel (class RegisterUser { public function __invoke(RegisterUserData $data) {...} }). Контроллер делает $action($data).
Преимущество над обычным методом handle(): можно DI-инжектить как callable, передавать в pipeline, использовать как value-object для команды.
::class от get_class($obj)?User::class — резолвится в compile-time в полное имя класса с namespace ("App\\Models\\User"). Работает с use-импортами, IDE подсказывает.
get_class($obj) — резолвится в runtime, возвращает класс конкретного экземпляра. Без аргумента (внутри метода) — текущий класс.
Когда что: ::class — для типов в сигнатурах, биндингов DI, attributes. get_class — когда нужно знать реальный класс instance (например, для логирования).
extract() и compact()?extract($arr) — создаёт переменные в текущем scope из ключей массива. Опасность: может перезаписать существующие переменные. Если массив от пользователя — security hole.
compact('a', 'b') — обратная операция: создаёт массив из переменных по их именам. Менее опасно, но IDE не подсказывает.
В современном коде их избегают: extract — почти никогда, compact — иногда в передаче в Blade-views (хотя Laravel рекомендует именованные массивы).
Классический singleton — статический getInstance() с private __construct. Проблема: global state, плохо тестируется (нельзя mockать), скрытая зависимость.
Альтернатива №1 — Service Container (DI): класс просто требует через конструктор: __construct(private Cache $cache). Container даёт всем нужным один и тот же экземпляр через singleton() биндинг.
Альтернатива №2 — параметризация: вместо «всегда один» — передавать инстанс явно. Тогда тесты могут передать mock.
В Laravel singleton почти всегда означает «singleton scope в контейнере», а не GoF-singleton.
Практика руками (PHP Core)
Микро-задачи (15 мин каждая) — разогрев
isTruthy($x) Постановка: Функция возвращает true только если $x НЕ входит в 6 false-y значений PHP. Покрой 10 тестами.
declare(strict_types=1);
function isTruthy(mixed $x): bool
{
// Самый простой и корректный способ — bool-cast
return (bool) $x;
}
use PHPUnit\Framework\TestCase;
class IsTruthyTest extends TestCase
{
public function testFalsyValues(): void
{
foreach ([false, 0, 0.0, "", "0", null, []] as $x) {
$this->assertFalse(isTruthy($x));
}
}
public function testTruthyTraps(): void
{
// Ловушки — должны быть true
$this->assertTrue(isTruthy("0.0")); // непустая строка!
$this->assertTrue(isTruthy("false")); // строка!
$this->assertTrue(isTruthy([0])); // непустой массив
$this->assertTrue(isTruthy(-1)); // любое не-ноль
}
}
"0.0", "false", [0] — НЕ false-y (это классическая ловушка). Не пиши свой список — (bool)$x точнее и короче.
strict_types и TypeError Постановка: Напиши функцию add(int $a, int $b): int. Вызови с '5' и '3' в двух режимах — со strict_types=1 и без. Объясни разницу.
// === файл strict.php ===
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
try {
echo add('5', '3');
} catch (TypeError $e) {
echo "Caught: " . $e->getMessage();
// "add(): Argument #1 ($a) must be of type int, string given"
}
// === файл loose.php (БЕЗ declare) ===
function add(int $a, int $b): int
{
return $a + $b;
}
echo add('5', '3'); // 8 (PHP молча привёл '5' и '3' к int)
declare(strict_types=1) работает только в файле, где написан. Не наследуется по include/autoload. Должен быть первым оператором файла (после <?php).
extractEmails($text) Постановка: Через regex извлеки все email-адреса из произвольного текста. Возврат — массив уникальных email в нижнем регистре.
function extractEmails(string $text): array
{
// Упрощённый regex (RFC 5322 — гораздо сложнее, но в 99% хватает)
$pattern = '/[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}/i';
preg_match_all($pattern, $text, $matches);
return array_values(array_unique(
array_map('strtolower', $matches[0])
));
}
// Использование
$text = 'Пиши на Alice@Example.com или bob@test.io. Дубль: alice@example.com';
print_r(extractEmails($text));
// ['alice@example.com', 'bob@test.io']
filter_var($email, FILTER_VALIDATE_EMAIL) точнее regex. array_unique сохраняет ключи — оборачивай в array_values. Флаг /i — case-insensitive, но в результат вернётся оригинальный регистр.
formatPrice($amount, $currency) Постановка: Форматирование цены в виде "$1,234.56 USD". Использовать number_format + sprintf.
function formatPrice(float $amount, string $currency): string
{
$symbol = match($currency) {
'USD' => '$',
'EUR' => '€',
'KZT' => '₸',
default => '',
};
// number_format($n, decimals=2, decSep='.', thousandsSep=',')
$formatted = number_format($amount, 2, '.', ',');
return sprintf('%s%s %s', $symbol, $formatted, $currency);
}
echo formatPrice(1234.5, 'USD'); // "$1,234.50 USD"
echo formatPrice(99.99, 'EUR'); // "€99.99 EUR"
echo formatPrice(5500, 'KZT'); // "₸5,500.00 KZT"
NumberFormatter::CURRENCY из ext-intl. В JSON-API лучше отдавать цена + валюта отдельно (минимальные единицы — копейки/центы), а форматировать на фронте.
Постановка и эталонные one-liner'ы (сделай свой вариант, потом сверь):
$users = [['name'=>'A'], ['name'=>'B'], ['name'=>'C']];
$names = array_map(fn($u) => $u['name'], $users);
// ['A', 'B', 'C']
// Через array_column короче:
$names = array_column($users, 'name');
$data = ['user_id'=>1, 'admin_id'=>2, 'name'=>'X'];
$ids = array_filter(
$data,
fn($k) => str_ends_with($k, '_id'),
ARRAY_FILTER_USE_KEY
);
// ['user_id'=>1, 'admin_id'=>2]
$orders = [['amount'=>100], ['amount'=>200], ['amount'=>150]];
$stats = array_reduce($orders, function($carry, $o) {
$carry['total'] += $o['amount'];
$carry['count']++;
$carry['avg'] = $carry['total'] / $carry['count'];
return $carry;
}, ['total'=>0, 'count'=>0, 'avg'=>0]);
// ['total'=>450, 'count'=>3, 'avg'=>150]
$prices = [100, 200, 300];
array_walk($prices, fn(&$p) => $p *= 1.1);
// [110, 220, 330] — массив изменён in-place
$users = [
(object)['name'=>'C', 'age'=>30],
(object)['name'=>'A', 'age'=>25],
(object)['name'=>'B', 'age'=>28],
];
usort($users, fn($a, $b) => $a->age <=> $b->age);
// отсортировано по возрасту: 25, 28, 30
$arr = ['name'=>'Alice', 'age'=>30, 'city'=>'NYC'];
['name' => $n, 'age' => $a] = $arr;
// $n = 'Alice', $a = 30 (без city)
// В function-signature (PHP 8.0+):
function process(['name' => $name]) { /* ... */ }
Готовые задания — полный разбор (30-60 мин)
Cacheable с защитой от null Постановка: Реализуй trait Cacheable со свойством $cache и методом remember($key, callable $cb). Acceptance: кэш работает даже когда callback возвращает null (большинство наивных реализаций ломаются именно тут).
- Объяви trait, добавь
private array $cache = []. - Метод
remember: проверь наличие ключа черезarray_key_exists(НЕisset!). - Если есть — верни закэшированное. Если нет — вызови callback, сохрани, верни.
- Подключи в
UserServiceчерезuse Cacheable. - Напиши тест с callback, возвращающим
null: убедись что callback вызывается ровно 1 раз.
trait Cacheable
{
private array $cache = [];
public function remember(string $key, callable $callback): mixed
{
// array_key_exists вместо isset — корректно работает с null
if (array_key_exists($key, $this->cache)) {
return $this->cache[$key];
}
return $this->cache[$key] = $callback();
}
public function forget(string $key): void
{
unset($this->cache[$key]);
}
}
class UserService
{
use Cacheable;
public function findById(int $id): ?array
{
return $this->remember("user.$id", function() use ($id) {
// Может вернуть null если пользователь не найден
return DB::find($id);
});
}
}
class CacheableTest extends TestCase
{
public function testCachesNullResult(): void
{
$service = new UserService();
$callCount = 0;
$callback = function() use (&$callCount) {
$callCount++;
return null; // валидное значение
};
$service->remember('k', $callback);
$service->remember('k', $callback); // должно взять из кэша
$service->remember('k', $callback);
$this->assertEquals(1, $callCount); // callback вызван только 1 раз
}
}
isset — НЕ сработает для null: isset($cache['k']) вернёт false если значение null, и callback вызовется снова (классический баг). Также если в классе уже есть $cache с другой видимостью — fatal error при use Cacheable.
TransactionGuard через RAII Постановка: Класс автоматически откатывает транзакцию, если не было явного commit(). Защита от забытого rollback при exception.
- Класс принимает
PDOв конструкторе. Сразу вызываетbeginTransaction(). - Свойство
$committed = false. - Метод
commit(): вызвать$pdo->commit(), поставить$committed = true. - Деструктор: если
!$committedи$pdo->inTransaction()—rollBack(). - Тест: внутри функции с
TransactionGuardкидаем exception, проверяем что транзакция откатилась.
final class TransactionGuard
{
private bool $committed = false;
public function __construct(private PDO $db)
{
$this->db->beginTransaction();
}
public function commit(): void
{
$this->db->commit();
$this->committed = true;
}
public function __destruct()
{
// если не было commit() и транзакция активна — откатываем
if (!$this->committed && $this->db->inTransaction()) {
$this->db->rollBack();
}
}
}
// Использование
function transferMoney(PDO $db, int $fromId, int $toId, int $amount): void
{
$tx = new TransactionGuard($db);
$db->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
->execute([$amount, $fromId]);
$db->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
->execute([$amount, $toId]);
// Любой throw здесь — транзакция откатится автоматически в __destruct
$tx->commit();
}
public function testAutoRollbackOnException(): void
{
$db = new PDO('sqlite::memory:');
$db->exec('CREATE TABLE t (v INT)');
try {
$tx = new TransactionGuard($db);
$db->exec('INSERT INTO t VALUES (1)');
throw new RuntimeException('oops');
$tx->commit(); // сюда не дойдёт
} catch (RuntimeException) {
// $tx уничтожен → __destruct → rollBack
}
$count = $db->query('SELECT COUNT(*) FROM t')->fetchColumn();
$this->assertEquals(0, $count); // данных нет — rollback сработал
}
TransactionGuard ссылается на другие объекты, которые тоже на GC — могут быть проблемы. В long-running процессах лучше явный finally { $tx->rollBackIfNeeded(); }. Также проверка $db->inTransaction() важна — без неё rollBack на уже зафиксированной транзакции бросит exception.
QueryBuilder через __call Постановка: Fluent API через магический __call. $qb->where(...)->orderBy(...)->limit(10)->toSql() — собирает SQL без явного объявления методов.
final class QueryBuilder
{
private array $wheres = [];
private array $orders = [];
private ?int $limit = null;
public function __construct(private string $table) {}
public function __call(string $name, array $args): self
{
match($name) {
'where' => $this->wheres[] = sprintf('%s %s %s',
$args[0], $args[1], $this->quote($args[2])),
'orderBy' => $this->orders[] = $args[0] . ' ' . ($args[1] ?? 'ASC'),
'limit' => $this->limit = (int) $args[0],
default => throw new BadMethodCallException("Unknown method: $name"),
};
return $this; // fluent chain
}
public function toSql(): string
{
$sql = "SELECT * FROM {$this->table}";
if ($this->wheres) {
$sql .= ' WHERE ' . implode(' AND ', $this->wheres);
}
if ($this->orders) {
$sql .= ' ORDER BY ' . implode(', ', $this->orders);
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
private function quote(mixed $v): string
{
return is_int($v) ? (string) $v : "'" . addslashes($v) . "'";
}
}
// Использование
$sql = (new QueryBuilder('users'))
->where('age', '>', 18)
->where('country', '=', 'KZ')
->orderBy('name')
->limit(10)
->toSql();
// "SELECT * FROM users WHERE age > 18 AND country = 'KZ' ORDER BY name ASC LIMIT 10"
addslashes в реальном коде — НЕ защита от SQLi! Используй prepared statements (PDO::prepare с параметрами). Магия через __call ломает IDE-автодополнение — поэтому Laravel QueryBuilder уже добавил явные методы. Магия хороша для прототипов / DSL, но в production хочется явных сигнатур.
PaymentInterface с двумя реализациями Постановка: Интерфейс + Stripe и PayPal реализации. Функция processOrder работает с любой через type-hint интерфейса.
interface PaymentInterface
{
public function charge(int $amountCents, string $currency): string; // возвращает transactionId
public function refund(string $transactionId): bool;
}
final class StripePayment implements PaymentInterface
{
public function __construct(private string $apiKey) {}
public function charge(int $amountCents, string $currency): string
{
// Реальный код: \Stripe\Charge::create(...)
return 'stripe_ch_' . uniqid();
}
public function refund(string $transactionId): bool
{
return str_starts_with($transactionId, 'stripe_');
}
}
final class PayPalPayment implements PaymentInterface
{
public function charge(int $amountCents, string $currency): string
{
return 'paypal_TX_' . strtoupper(uniqid());
}
public function refund(string $transactionId): bool
{
return str_starts_with($transactionId, 'paypal_');
}
}
// Полиморфизм — функция работает с ЛЮБОЙ реализацией
function processOrder(PaymentInterface $gateway, int $cents): string
{
return $gateway->charge($cents, 'USD');
}
// Один и тот же код — две разные платёжки
$stripeId = processOrder(new StripePayment('sk_test_...'), 1000);
$paypalId = processOrder(new PayPalPayment(), 1000);
int) — никогда float! 0.1 + 0.2 !== 0.3 в плавающей точке. Reusable check: интерфейс должен быть достаточно широким (Stripe и PayPal оба умеют refund, capture, partial refund, idempotency_key — учти на проектировании).
BaseRepository через PDO Постановка: Abstract класс с готовыми find($id), all(). Наследники определяют только table(). Демонстрация шаблонного метода.
abstract class BaseRepository
{
public function __construct(protected PDO $db) {}
// Подкласс ОБЯЗАН реализовать — какая таблица
abstract protected function table(): string;
public function find(int $id): ?array
{
$stmt = $this->db->prepare(
"SELECT * FROM {$this->table()} WHERE id = :id"
);
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function all(int $limit = 100): array
{
$stmt = $this->db->prepare(
"SELECT * FROM {$this->table()} LIMIT :limit"
);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function delete(int $id): bool
{
$stmt = $this->db->prepare(
"DELETE FROM {$this->table()} WHERE id = :id"
);
return $stmt->execute(['id' => $id]);
}
}
// Наследники определяют ОДИН метод
final class UserRepository extends BaseRepository
{
protected function table(): string { return 'users'; }
}
final class OrderRepository extends BaseRepository
{
protected function table(): string { return 'orders'; }
}
// Использование — find/all/delete бесплатно во всех
$users = new UserRepository($pdo);
$user = $users->find(1);
$all = $users->all(50);
table() возвращал литерал, а не user input. LIMIT в PDO с MySQL надо биндить через PARAM_INT — иначе lexer воспримет число как строку и SQL сломается.
Мини-проекты в стиле LeetCode (1-2 часа)
my_array_reduce Постановка: Реализуй my_array_reduce(array $arr, callable $cb, $initial). Поведение должно совпасть со встроенной 1-в-1 на 5 кейсах.
function my_array_reduce(array $arr, callable $callback, mixed $initial = null): mixed
{
$accumulator = $initial;
foreach ($arr as $item) {
// Жёстко зашитый порядок: 1-й arg = аккумулятор, 2-й = элемент
$accumulator = $callback($accumulator, $item);
}
return $accumulator;
}
// 1. Сумма
assert(my_array_reduce([1,2,3,4], fn($a,$b) => $a+$b, 0) === 10);
// 2. Произведение
assert(my_array_reduce([1,2,3,4], fn($a,$b) => $a*$b, 1) === 24);
// 3. Max
assert(my_array_reduce([3,7,2,9,5], fn($a,$b) => $a > $b ? $a : $b, 0) === 9);
// 4. Построение assoc-массива
$users = [['id'=>1,'name'=>'A'], ['id'=>2,'name'=>'B']];
$indexed = my_array_reduce($users, function($c, $u) {
$c[$u['id']] = $u['name'];
return $c;
}, []);
assert($indexed === [1=>'A', 2=>'B']);
// 5. Пустой массив → возвращает initial
assert(my_array_reduce([], fn($a,$b) => $a+$b, 99) === 99);
Collection (immutable) Постановка: Класс с методами map / filter / reduce / first / count / toArray / toJson. filter и map возвращают новую Collection (immutable).
final class Collection implements Countable, IteratorAggregate
{
public function __construct(private readonly array $items = []) {}
public static function of(array $items): self
{
return new self($items);
}
public function map(callable $fn): self
{
return new self(array_map($fn, $this->items));
}
public function filter(callable $fn): self
{
return new self(array_values(array_filter($this->items, $fn)));
}
public function reduce(callable $fn, mixed $initial): mixed
{
return array_reduce($this->items, $fn, $initial);
}
public function first(): mixed
{
return $this->items[array_key_first($this->items)] ?? null;
}
public function count(): int
{
return count($this->items);
}
public function toArray(): array { return $this->items; }
public function toJson(): string { return json_encode($this->items); }
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->items);
}
}
// Acceptance-тест
$result = Collection::of([1,2,3])
->map(fn($x) => $x * 2)
->filter(fn($x) => $x > 2)
->toArray();
assert($result === [4, 6]);
readonly array — иммутабельный, но если в массиве лежат объекты, они НЕ становятся readonly. Это shallow immutability. array_filter сохраняет ключи — оборачиваем в array_values чтобы collection всегда была sequential.
Mini Project 3: Свой Validator
Validator::make($data, ['email' => 'required|email', 'age' => 'required|int|min:18']) → массив ошибок или пусто. Реализуй 5 правил: required, email, int, min, max. Архитектура: правила = классы, реализующие RuleInterface с методом passes($value): bool. Validator парсит строку правил → создаёт массив объектов → прогоняет каждое. Тесты: 5 кейсов (ок / невалидный email / возраст <18 / отсутствует поле / тип не int).
Mini Project 4: Свой DI-контейнер
Класс Container с bind(string $abstract, callable $factory), singleton(...), resolve(string $abstract). Поддержи auto-resolution через рефлексию: если resolve('App\Service') и в конструкторе зависимость — рекурсивно резолвится. Acceptance: $c->bind('Logger', fn() => new FileLogger()), $c->resolve('Service') создаёт сервис с инжектированным Logger.
Mini Project 5: Mini-ORM (Active Record)
Abstract Model со static find($id) / all(), instance save() / delete(). Использует __get/__set для динамических атрибутов из БД (магия!). Наследник User extends Model определяет только protected static $table = 'users'. Acceptance: $u = User::find(1); $u->name = 'X'; $u->save(); работает. Это упрощённая модель Eloquent — поймёшь Laravel «изнутри».
План применения на 2 недели
- Неделя 1: Все 10 микро-задач (по 2/день) + готовые задания 1-2 (Cacheable + TransactionGuard).
- Неделя 2: Готовые задания 3-5 (Builder + PaymentInterface + BaseRepository) + Mini Project 2 (Collection).
- За 3 дня до финальной проверки: прогон 45 вопросов из «Вопросник» вслух, таймер 90 сек.
- День перед: распечатать Шпаргалку, ещё раз пробежать ключевые таблицы.
Это задания напрямую отрабатывают то, что спросят. Mini Project 2 (Collection) — её часто разбирают через Eloquent Collection: «как реализована filter?» — ты будешь знать на собственном коде.