PHP Core Knowledge Base

Подробное руководство по PHP для backend-разработчиков

1. Типы данных PHP

Основные типы (Basic Types)

В PHP существует 8 основных типов данных. Каждый тип имеет свои особенности и правила преобразования.
Тип Описание Примеры
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

PHP — язык с динамической типизацией: тип переменной определяется значением, а не объявлением. Преобразования типов происходят двумя путями. Type Juggling — PHP сам конвертирует типы «на лету» (при арифметике, сравнении, логических операциях). Type Casting — разработчик явно указывает целевой тип через (тип)$value. Различие в инициаторе и контроле: juggling непредсказуем в сложных выражениях, casting даёт полный контроль.
Type Juggling в арифметике
// Строка + число — строка превращается в число
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"
Type Juggling в сравнениях — поведение зависит от версии PHP
// === 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)
Что изменилось в PHP 8.0 (RFC: "Saner string to number comparisons"):
  • До 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).
Type Casting — явное приведение
// (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
Объект vs ассоциативный массив — сравнение
АспектАссоциативный массивОбъект
Что этоУпорядоченная коллекция пар «ключ → значение»Экземпляр класса с свойствами (и методами)
Доступ к данным$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_existsisset($obj->name) / property_exists
var_dumparray(2) { ["name"]=> ... }object(stdClass)#1 (2) { ["name"]=> ... }
Сериализация в JSONjson_encode — JSON-объектjson_encode — тоже JSON-объект
Тип в type-hintarrayobject / конкретный класс
ПроизводительностьЧуть быстрееЧуть медленнее, но разница не критична
Когда что использовать:
  • Ассоциативный массив — конфиги, простые 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-байты для скрытия инкапсуляции.
(array)$object с разной видимостью
// 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"] технически работает,
// но нарушает инкапсуляцию — не используйте в нормальном коде.
Для безопасной сериализации объектов в массив используйте геттеры, Reflection API, либо get_object_vars() (вернёт только видимые из текущей области свойства, без null-байтов). Прямое (array) подходит только для stdClass или внутри тех же классов.
__toString для объектов
// (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. Один из них (=) вообще не сравнение, остальные различаются в том, приводит ли PHP типы перед сравнением. Правильный выбор между ними определяет, сработает ли ваше условие так, как вы думаете.
1. = — присваивание (assignment)
// Действие, не сравнение: кладёт значение справа в переменную слева
$x = 5;             // $x теперь равно 5

// Возвращаемое значение присваивания = присвоенное значение
if ($x = 10) {     // ОПАСНО! Присваивает 10, не сравнивает.
                    // Условие всегда true (10 — truthy).
                    // Большинство IDE/линтеров подсветят это как warning.
}
Использовать = внутри условий допустимо только осознанно (например, для inline-присваивания: while (($row = $stmt->fetch()) !== false) { ... }). Обычная ошибка — написать = вместо ==/===.
2. == — нестрогое равенство
// Сравнивает значения после приведения типов (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)
3. === — строгое равенство (рекомендуется по умолчанию)
// Сравнивает И значение, И тип — без приведения
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)
4. != и <> — нестрогое неравенство
// Противоположность == — 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
5. !== — строгое неравенство
// Противоположность === — 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 операторы (+=, -=, *=, /=, %=, .=)

Это составные операторы присваивания (compound assignment operators): сокращённая запись для «применить операцию и записать результат обратно в ту же переменную». Стандартный синтаксис в PHP, C, JavaScript, Python и большинстве С-подобных языков.
Все compound операторы PHP
// Каждая пара эквивалентна:

// += (сложение / конкатенация чисел)
$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То же что ||, но НИЖЕ приоритетсм. ловушку нижеНизкий
xortrue если РОВНО ОДИН$a xor $b — взаимоисключающее ИЛИНизкий
Таблица истинности AND, OR, XOR
ABA && BA || BA xor B
truetruetruetruefalse
truefalsefalsetruetrue
falsetruefalsetruetrue
falsefalsefalsefalsefalse
Короткое замыкание (short-circuit evaluation)
// 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 ??.
1. Тернарный условие ? 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');
2. Elvis ?: — сокращённый тернарный
// Если средняя часть тернарного — то же что условие, можно её опустить:

// Длинная форма:
$name = $input ? $input : 'Anonymous';

// Elvis (сокращённая):
$name = $input ?: 'Anonymous';
// Если $input истинно — вернёт $input, иначе 'Anonymous'.

// Работает по TRUTHY: '0', '', 0, null, [] — все вернут 'Anonymous'.
// Если нужно проверять именно null — используй ??.
3. Null coalescing ?? (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. Иначе оставит как есть.
4. Nullsafe ?-> (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» — точный ответ ниже.
1. Return type declaration — двоеточие перед типом
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.
Поддерживаемые return type-hints
ТипЧто означаетВерсия
: voidФункция НИЧЕГО не возвращает (нельзя return $x)7.1+
: int / : float / : string / : bool / : arrayСкалярные / массив7.0+
: ?intNullable: int или null7.1+
: self / : staticВозврат текущего класса (для fluent)7.0+ / 8.0+
: UserОбъект класса User7.0+
: iterablearray или объект Traversable7.1+
: neverФункция НЕ возвращает управление (throw / exit)8.1+
: int|string (union)Несколько типов на выбор8.0+
: Countable&Iterator (intersection)Реализует ВСЕ перечисленные8.1+
2-5. Другие контексты двоеточия
// 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 Декларация

strict_types=1 требует точное совпадение типов при передаче аргументов. Это должно быть первым оператором в файле!
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
В Laravel и современных проектах ВСЕГДА используй declare(strict_types=1) для типобезопасности!

Union Types и Nullable (PHP 8+)

PHP 8+ позволяет указывать несколько возможных типов и явно маркировать nullable значения.
Union Types
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+)

Intersection типы требуют чтобы объект реализовал ВСЕ указанные интерфейсы.
Intersection Types
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);   // Возвращает строку с типом
isset() возвращает false если переменной нет или она null. empty() также возвращает true для 0, "", false, null. Используй !isset() или ?? оператор для более точных проверок.

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']}"
Для доступа к элементам массива в строке используй {$array['key']}, иначе PHP не поймёт синтаксис!

Heredoc и Nowdoc — многострочные строки

Два способа создавать многострочные строки в PHP. Удобны, когда нужно написать большой текст с кавычками или переменными без конкатенации и экранирования. Различаются только одним: Heredoc интерпретирует переменные (ведёт себя как двойные кавычки), Nowdoc — нет (ведёт себя как одинарные).
Heredoc — с интерпретацией
// Синтаксис: <<<ИДЕНТИФИКАТОР (без пробелов после <<<)
$name = "Анна";
$text = <<<EOT
Привет, $name!
Это многострочный текст.
Поддерживает переносы и "кавычки" без экранирования.
EOT;

echo $text;
// Вывод:
// Привет, Анна!
// Это многострочный текст.
// Поддерживает переносы и "кавычки" без экранирования.
Nowdoc — без интерпретации (литерально)
// Синтаксис: <<<'ИДЕНТИФИКАТОР' (в одинарных кавычках)
$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+ Функции строк

PHP 8 добавил удобные функции для работы со строками вместо preg_match.
str_contains, str_starts_with, str_ends_with
// Проверить содержит ли строка подстроку
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)

Для работы с Unicode и не-ASCII символами используй mb_string функции вместо обычных str функций.
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);  // Первая буква заглавная
ВСЕГДА используй mb_* функции при работе с пользовательским вводом, особенно если это может быть не-ASCII! Это критично для интернациональных приложений.

Регулярные выражения (Regex)

preg_match и preg_replace
// 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
}
Разбор регулярки для 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)
Почему if (!preg_match(...))
// 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   — нет символа @
Эта regex покрывает 99% случаев, но не валидирует email на 100% по RFC 5321/5322. Для боевой валидации в Laravel используйте 'email' => 'required|email:rfc,dns' — правило email:rfc проверяет полную спецификацию, dns ещё и резолвит домен.
preg_match_all — найти ВСЕ совпадения
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"]          — вторая группа
Шпаргалка спецсимволов regex:
\d — цифра, \D — не цифра
\w — буква/цифра/подчёркивание, \W — всё остальное
\s — пробельный символ (пробел/таб/перевод), \S — не пробельный
. — любой символ, \. — буквальная точка
+ — 1 или больше, * — 0 или больше, ? — 0 или 1
{n} — ровно n раз, {n,m} — от n до m, {n,} — n или больше
^ — начало строки, $ — конец строки
[...] — набор символов, [^...] — кроме набора
(...) — захватывающая группа, (?:...) — группа без захвата

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

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 ассоциативный

В PHP массив — это упорядоченная коллекция пар «ключ → значение». В отличие от других языков (где разные структуры — list, dict, tuple), в PHP один тип 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 {}).

Под капотом в PHP — один тип 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 контекстов использования

В PHP оператор => (его внутреннее имя — T_DOUBLE_ARROW) встречается в шести разных контекстах. Это не оператор сравнения (для этого ==, ===) и не общая «стрелка» как в JavaScript. Семантика в каждом контексте разная, но синтаксис один.
1. Массивы — пара «ключ => значение»
$arr = [
    'name' => 'Иван',
    'age'  => 30,
    5      => 'пятый',   // ключ может быть числом
];
Частая ошибка: путают => внутри литерала vs = при добавлении в массив.

Это два разных синтаксиса для разных операций:

Различие: создание массива 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']] — это попытка создать литерал внутри индексатора, что синтаксически невозможно.

2. foreach — перебор с ключами
foreach ($arr as $key => $value) {
    echo "$key => $value";
}
// Без => получили бы только значения:
// foreach ($arr as $value) { ... }
3. Стрелочные функции (fn) — PHP 7.4+
// 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)
4. match-выражение — PHP 8.0+
// => разделяет проверяемое значение и возвращаемый результат
$result = match($status) {
    200           => 'OK',
    404           => 'Not Found',
    500, 502, 503 => 'Server Error',
    default       => 'Unknown',
};

// В отличие от switch:
//   — строгое сравнение (===), не нестрогое
//   — возвращает значение (можно присвоить в переменную)
//   — обязателен default или match попадание (иначе UnhandledMatchError)
5. Генераторы (yield) — выдача пар ключ-значение
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...
6. Симметричная деструктуризация массива — PHP 7.1+
// Распаковка ассоциативного массива в переменные с указанием ключей
$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 подсказывает поля, видны опечатки.
Что => НЕ значит в PHP
// Не оператор сравнения — для этого == или ===
$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 - Преобразование элементов

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()
Эквивалент через foreach (что под капотом)
// array_map "под капотом":
$result = [];
foreach ($numbers as $key => $value) {
    $result[$key] = $callback($value);
}

// Когда foreach лучше:
//   — нужны побочные эффекты (логирование, запись в БД)
//   — внутри тела > 1 строки кода
//   — нужно прервать цикл (break/continue) — у array_map нельзя
array_map + array_sum vs array_reduce — какой выбрать
// Вариант 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 - Фильтрация элементов

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 — последняя цифра числа.
Три режима array_filter — флаги USE_KEY и USE_BOTH
$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 — когда условие зависит и от ключа, и от значения одновременно.
toArray() — это метод коллекции, не хелпер
// В строке $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();
Хелпер vs метод в Laravel:
— Хелперы — глобальные функции: collect(), dd(), view(), config(), route(), now(), auth(). Вызываются без объекта.
— Методы — определены в классах, вызываются через ->: $users->toArray(), $user->save(), $collection->filter().
Полный справочник хелперов — в KB_10 «Хелперы & методы».

array_reduce - Свёртка массива

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».
Пошаговая трассировка для $orders = [100, 200, 150]
+------+---------+-------------+------------------------------------------+
| Шаг  | $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,
]
Почему `avg` пересчитывается на каждой итерации? Технически — излишне: достаточно посчитать его один раз после цикла ($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 — императивный, легче дебажить
Защита от деления на 0: в array_reduce варианте, если массив $orders пустой, цикл не пройдёт, callback не вызовется — $stats['avg'] останется 0 из начального значения. В foreach-варианте без проверки $count > 0 произошло бы DivisionByZeroError. Это редкий случай, когда array_reduce «бесплатно» защищает от пограничного бага.
Пошаговый разбор `$sum = array_reduce($numbers, fn($carry, $item) => $carry + $item, 0)` для [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.
Как PHP «понимает» что первый параметр — аккумулятор, а второй — элемент? Не по именам переменных. $carry и $item — имена, придуманные разработчиком, они могут быть любыми ($x, $y, $a, $b). Важен порядок: первым аргументом callback всегда получает аккумулятор, вторым — текущий элемент. Это зашито в реализацию array_reduce на уровне ядра PHP.
Псевдо-реализация array_reduce (что внутри функции)
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;
}
Эквивалент с обычным foreach
$sum = 0;
foreach ($numbers as $item) {
    $sum += $item;
}
// результат тот же: 10
// array_reduce — более декларативный способ, особенно для построения структур
Зачем начальное значение (3-й параметр)?
— Если массив пустой → array_reduce вернёт начальное значение (для 0 это логичная сумма, для 1 — произведение).
— Если массив не пустой → начальное значение участвует в первом вычислении (0 + 1 = 1 в нашем примере).
Без начального значения по умолчанию — null, что часто ломает callback (null + 1 = warning + 1).
Когда НЕ нужен array_reduce — есть встроенные функции
// Для типовых операций 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_reduce
ЗадачаИспользоватьПочему
Суммаarray_sum($arr)Встроено, быстро, читаемо
Произведениеarray_product($arr)Встроено
Max / Minmax($arr) / min($arr)Встроено
Среднее (avg)array_sum / countДвух функций достаточно
Сумма + count + avg за один проходarray_reduce1 итерация вместо 2-3
Построить assoc [id => name]array_reduce или array_columnReduce — гибче, column — короче
Сложная агрегация (sum чётных + max нечётных)array_reduceСвоя логика в callback
Любая нестандартная свёрткаarray_reduceУниверсальный инструмент
Правило: когда видишь задачу «свернуть массив в одно значение» — сначала проверь, есть ли встроенная функция. Если есть (sum/product/max/min/count) — используй её. array_reduce — это универсальный fallback для случаев когда встроенной нет или нужно несколько метрик за один проход.

array_walk - Изменение элементов с побочными эффектами

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). Без него параметр — это копия значения; изменения внутри функции не затронут оригинал. Со & — параметр становится псевдонимом самого исходного элемента; всё, что делается с переменной внутри функции, изменяет исходные данные.
Разница: с & vs без &
// Без & — копия, оригинал не меняется
$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] — массив реально изменился
& работает в любой функции, не только array_walk
// Без & — без эффекта
function addOneCopy($num) {
    $num++;
}
$x = 5;
addOneCopy($x);
echo $x;    // 5 — $x не изменился

// С & — функция мутирует переданную переменную
function addOneRef(&$num) {
    $num++;
}
$x = 5;
addOneRef($x);
echo $x;    // 6 — $x изменился, потому что передан по ссылке
Где ещё встречается & в PHP
// 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
Классический pitfall — забытая ссылка после foreach
$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 примеры
// 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 и я разберусь».
Что возвращает $a <=> $b
// -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() или магию класса
Зачем нужны именно -1/0/1 для usort
// 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']));
Multi-criteria сортировка через цепочку <=> и ?:
// Сортировать сначала по возрасту, потом по имени.
// Идея: первое не-нулевое <=> и есть итоговый результат.
// ?: — короткий тернарный (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)
Связанные функции сортировки в PHP:
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 и распаковка
// 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() под капотом:
  1. Выделяет память для нового объекта (с местом под все объявленные свойства + служебная информация).
  2. Запускает __construct(), если он определён в классе.
  3. Возвращает ссылку на созданный объект (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;
Сравнение: класс vs объект
АспектКлассОбъект
Что этоОписание (чертёж, рецепт, тип)Экземпляр (конкретный дом, пирог) в памяти
ПамятьНе выделена под данныеВыделена — хранит реальные значения свойств
Как «создать»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 (тот объект, который создаётся)
Зачем нужен $this: без него PHP не различит локальную переменную метода и свойство объекта. $name — локальная переменная (аргумент или просто объявленная в методе). $this->name — свойство объекта. Это разные сущности с одинаковым именем.
$name vs $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", потом установит свойство
$this для вызова других методов того же объекта
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();
    }
}
return $this — fluent interface (method chaining)
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
Где $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 — constructor property promotion (8.0+)
// До 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->.
В конструкторе — $this->поле = $аргумент присваивает значение свойству объекта.
return $this в setter'ах — даёт fluent interface (method chaining).
В static-методах $this отсутствует — используйте self:: или static:: для доступа к классу.
В замыканиях по умолчанию нет — используйте Closure::bind или стрелочные функции (fn() => $this->x внутри метода захватят $this автоматически).
С PHP 8.0+ для конструкторов используйте promoted properties — короче и без повторов.

Когда писать $, а когда нет — все формы доступа к свойствам

Главная путаница новичков в PHP: «почему private $items с долларом, а $this->items без?». PHP имеет 5 разных синтаксических форм работы с именем свойства/переменной. Разберём каждую.
ФормаЧто этоГде используется
$itemsЛокальная переменнаяВнутри функции/метода. Существует только в этом scope.
private array $items = []Объявление свойства классаТело класса. $ здесь — часть имени переменной.
$this->itemsДоступ к свойству объектаВнутри нестатического метода. $ только у $this, у items нет.
$this->$varNameVariable property — имя свойства в переменнойРедкая магия / опасный паттерн. Если $varName = 'items', то $this->$varName = $this->items.
self::$itemsStatic свойствоВнутри методов класса. У static свойств $ остаётся (отличие от обычных!).
items (без $ и без ->)КонстантаPHP ищет const items. Если нет — warning + строка 'items' (deprecated, в PHP 8.0 → Error).
Все 6 форм в одном файле
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:: — классовые члены

Static (статические свойства и методы) принадлежат самому классу, а не отдельным объектам. Они существуют в единственном экземпляре независимо от того, сколько объектов создано. Вызываются через оператор :: без new.

self и static — два способа сослаться на класс изнутри его кода. Разница между ними проявляется при наследовании.
Базовый пример: счётчик через 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 — один счётчик на всё приложение.
Оператор :: (Scope Resolution Operator, «Paamayim Nekudotayim»)
// Доступ через :: применяется для:

// 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" — это синтаксическая ошибка с ::
self:: vs static:: — главное отличие (Late Static Binding)
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.
Шаги выполнения ChildClass::testSelf()
┌─────────────────────────────────────────────────────────────┐
│ 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.
Для контраста — шаги ChildClass::testStatic()
┌─────────────────────────────────────────────────────────────┐
│ 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 — как каждый раз заново звонящий курьер: смотрит на наклейку «кому» прямо сейчас. Адрес = «кто вызвал».
Что было бы, если бы testSelf() был объявлен в ChildClass
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:: такого эффекта нет: значение всегда берётся из класса, где написан код.
Полиморфизм через static:: на переопределяемом свойстве
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:: в базовом
   классе для всего, что наследники могут переопределять.
Связь с Eloquent. Именно поэтому Laravel 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.
Практический пример: Factory method в Laravel Eloquent
// В 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-фабрикам.
parent:: — вызов метода родителя
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;       — ошибка, через -> только свойства
Когда использовать static вообще
// Утилитарные функции без состояния
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

Access Modifiers
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_dump
var_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)$object: когда кастишь объект в массив через (array)$user, получаешь те же ключи, но с null-байтами: "\0*\0email" (protected) и "\0User\0password" (private). Подробно — в разделе 1, подсекция «(array)$object с разной видимостью». var_dump показывает в человекочитаемой нотации (:protected, :"Class":private), а внутри объекта PHP хранит именно через null-байты.

Типизированные свойства (PHP 7.4+)

С PHP 7.4 свойства можно типизировать прямо в объявлении. До этого тип проверялся только в сеттерах/конструкторе вручную. После — PHP сам выкинет TypeError при попытке записать неправильный тип.
Синтаксис: видимость + тип + имя + дефолт
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) — что писать с большой буквы

Стандарт PSR-12 — общепринятый в Laravel, Symfony, всех приличных PHP-проектах. PHP сам **case-insensitive** к именам функций и методов ($user->AddItem() и $user->addItem() одинаково работают), но IDE / линтер / collega на code review подсветят несоответствие стандарту.
ЧтоСтильПример
Класс / Интерфейс / Trait / EnumPascalCaseUser, OrderRepository, PaymentInterface, HasFactory
Метод / функцияcamelCaseaddItem(), getTotal(), findByEmail()
Свойство / переменнаяcamelCase$items, $totalAmount, $createdAt
Константа классаUPPER_SNAKEMAX_ITEMS, STATUS_ACTIVE
Файл с классомPascalCase (как класс)User.php, OrderRepository.php
NamespacePascalCase по сегментамApp\Models, Illuminate\Support
Маршрут / URLkebab-case/user-profile, /order-details/{id}
База: таблица / колонкаsnake_caseusers, created_at, email_verified_at
Типичные ошибки которые увидишь на code review
// Метод в 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 {}
Важно: если назовёшь метод PascalCase или класс lowercase — это **первый звоночек что код не пишешь в production**. Не сразу зарежут, но запомнят. Инструмент защиты — php-cs-fixer или laravel-pint автоматически приведёт к PSR-12 при коммите.

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
Чтоechoprint
ТипЯзыковая конструкцияЯзыковая конструкция
ВозвращаетНичего (void)Всегда 1 (можно использовать в выражении)
Несколько аргументовecho $a, $b, $cтолько один
СкоростьЧуть быстрееЧуть медленнее (из-за return)
Правило: для вывода используй echo без скобок. Для вывода объекта через метод — echo $obj->method();. Скобки вокруг echo создают иллюзию функции и могут сломать код через год (например, при добавлении второго аргумента).

Глоссарий: Инкапсуляция, Инвариант, DDD, Aggregate Root

Эти 4 термина часто путают, особенно «инкапсуляция = private». На самом деле это разные уровни абстракции: язык → принцип → паттерн → методология. Разберём по уровням.
ТерминУровеньОпределениеПример
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 есть, но инвариант не защищён → инкапсуляция СЛАБАЯ.
Aggregate Root в реальном коде Laravel
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 для массивов)
    }
}
3 признака настоящего Aggregate Root:
  • 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 — паттерн «инкапсулированное состояние»

Когда у класса есть private $items / $children / $orders и единственный способ их менять — через свои методы (addItem(), removeItem()), это паттерн Aggregate Root из DDD. Класс владеет коллекцией и защищает её инварианты.
Шаблон Aggregate Root
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);
    }
}
3 признака правильного Aggregate Root:
  • State в private — никто снаружи не может написать $cart->items[] = $bad в обход проверок.
  • Commands vs Queries разделены (CQS): команды меняют состояние и возвращают void, queries только читают и возвращают данные.
  • Инварианты защищены — проверки внутри команд (count < max, валидация и т.п.), не дают создать «невалидное» состояние.

На уровне middle+ часто спрашивают «как ты защищаешь инварианты» — этот паттерн и есть ответ. См. также KB_5 Architecture → DDD basics.

Derived value: Cached vs Computed — два пути для 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
        ));
    }
}
Trade-off — что выбрать
АспектCached (хранить)Computed (считать)
Чтение total()O(1)O(n) — пройти весь массив
Запись (add/remove)O(1) + обновить totalO(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 методы и свойства

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::

static:: и 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)
Используй static:: для полиморфного поведения в подклассах, self:: когда нужно явно указать текущий класс. Это особенно важно в laravel Models и Factory паттернах!

final и readonly (PHP 8.1+)

final и readonly
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 НЕ влияет на свойства (нельзя сделать значения "неизменяемыми").
readonly НЕ влияет на методы (нельзя сделать "нельзя переопределить").
final class — нельзя даже наследовать
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 — явная декларация что класс закрыт для расширения
// — Защита от хрупких иерархий: лучше явный отказ от наследования, чем глубокое дерево классов
readonly можно «обойти» переопределением в подклассе
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 class — PHP 8.2+
// Помечает ВСЕ свойства класса как 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 не запрещает изменение свойства.

Наследование и полиморфизм

Зачем нужно переопределение (override). Это возможность дочернего класса заменить или дополнить реализацию метода, унаследованного от родителя. Без переопределения вся иерархия была бы обречена на одинаковое поведение — смысла наследовать почти не было бы. Четыре основные причины:
  • Расширение — делаем то же, что родитель, плюс что-то своё (через parent::method());
  • Замена — полностью переписываем логику под нужды наследника;
  • Специализация — общий алгоритм подстраивается под конкретный случай;
  • Полиморфизм — код, работающий с родительским типом, автоматически получает поведение наследника.
Классический пример: Animal / Dog / Cat
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 — все три
// вывели бы "Какой-то звук" (поведение родителя).
Расширение через parent:: — не замена, а дополнение
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() — забыли бы сохранение в БД.
Специализация: переопределение под частный случай (Laravel)
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
// Если автор базового класса уверен, что метод НЕ должен меняться
// в наследниках — помечает его 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 — способ сказать: "Этот алгоритм критичен, не трогай."
Полиморфизм в production-коде (Strategy pattern)
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).
Когда переопределение невозможно — 6 ситуаций. Override работает только когда метод родителя виден подклассу и разрешён к изменению. Любое из условий ниже блокирует override.
1. Метод объявлен как final
class Database {
    final public function connect(): void { ... }
}

class MysqlDatabase extends Database {
    public function connect(): void { }    // Fatal error: Cannot override final method
}
// final — явное запрещение от автора базового класса.
2. Класс родителя объявлен как final
final class Money { ... }

class SpecialMoney extends Money { }
// Fatal: Class SpecialMoney cannot extend final class Money

// Невозможно даже создать подкласс — значит и override невозможен.
// Часто применяется для Value Object: Money, Email, UserId.
3. private метод — это НЕ override, это новый метод
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, если хотите чтобы наследники могли подменить.
4. Несовместимые сигнатуры — Liskov violations
// Даже если метод не 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
}
5. static методы — переопределить можно, но это другое
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
}
6. Метод НЕ существует в родителе — это просто добавление, не override
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
+-----------------------------------+----------------------+--------------------+
| Условие                           | Можно ли 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]  |
+-----------------------------------+----------------------+--------------------+
Главное правило: 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 — только через подкласс.
Реальный пример 1: Animal / Dog / Cat — основа полиморфизма
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();   // "Гав!"  — собственная реализация
Реальный пример 2: Template Method — общий CRUD-контроллер
// Каркас алгоритма фиксирован в родителе; шаги — в наследниках

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.
// Каждый наследник лишь указывает модель и правила. Если завтра добавится
// логирование запросов — добавляется в одном месте, работает для всех.
Реальный пример 3: NotificationService — каркас + контракт send()
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();   // каждый отрабатывает по-своему, логирование общее
}
Реальный пример 4: BaseEntity — общая модельная логика
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 раз; каждый наследник без копипасты получает поиск.
Реальный пример 5: SPL FilterIterator — встроенная PHP-абстракция
// 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+)

Default методы
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 - способ повторного использования кода в классах. Это как "горизонтальное наследование". Класс может использовать несколько traits, но может наследовать только один класс.
Базовые 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

Если несколько traits имеют методы с одинаковым именем, нужно явно указать какой использовать.
Конфликты методов
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

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();
}
Traits отлично подходят для кроссэффективности, которая НЕ является частью иерархии. Используй их для логирования, кэширования, валидации и других "горизонтальных" обязанностей!

Пояснялка — частые вопросы по разделу

① Что такое 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, когда деструктор не подходит — см. «Вопросник / Объяснялка» в конце раздела.
RAII — реальные сценарии __construct + __destruct
// 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)
__get/__set отлично подходят для ленивой загрузки данных, валидации, логирования доступа к свойствам. Это использует Laravel Eloquent — поля модели хранятся в $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'];
    }
}
__debugInfo и __serialize очень полезны для безопасности - скрывают чувствительные данные от отладки и сохранения. Используй их для password, API tokens, и прочих secrets!

Пояснялка — частые вопросы по разделу

① Откуда в примере 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.

Магические методы срабатывают в двух случаях:

  1. Свойство не объявлено вообще — как $user->name в примере. PHP не находит свойство → вызывает __set() / __get() и не выдаёт ошибку.
  2. Свойство объявлено, но недоступно в текущем контексте — например, 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

Namespaces избегают конфликты имен и организуют код логически. Объявление должно быть первым оператором в файле!
Базовые 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

PSR-4 - стандарт для автозагрузки классов. Composer автоматически генерирует автолоадер на основе конфигурации.
composer.json пример
{
    "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"
    }
}
PSR-4 правило: Namespace должна соответствовать директории. Например:
// Файл: 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 автоматически
    }
}

Автоматическая загрузка классов

Как работает autoloader
// После 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
Когда создаешь новый класс, убедись что его namespace соответствует директории. Composer автоматически найдет и загрузит класс по имени!

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

Разбор throw new InvalidArgumentException(...) построчно

Эта конструкция объединяет 4 концепции PHP. Если разобрать каждое слово — становится очевидно, что код делает и почему.
Что значит каждое слово
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)
Полный поток: throw → try/catch/finally
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.
  • Имя класса исключения = тип проблемы. Часто спрашивают разницу InvalidArgumentException vs RuntimeException — первое значит «программист передал плохие данные», второе значит «среда подвела (БД упала)».
  • Не лови Exception огульно — лови конкретный тип. Иначе скроешь баги, которые надо чинить.

Exception Hierarchy

PHP имеет иерархию исключений. Все исключения наследуют от Throwable (PHP 7+).
Тип Описание Примеры
Throwable Базовый интерфейс для всех -
Exception Обычные исключения (код ошибка) InvalidArgumentException
Error Ошибки PHP (не всегда ловятся) TypeError, DivisionByZeroError
ParseError Синтаксическая ошибка eval() с плохим синтаксисом
Try-Catch-Finally
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";
}

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

Custom exceptions
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);
Используй try/catch для предсказуемых ошибок. Используй set_exception_handler для ловли всех необработанных исключений. В production, логируй все ошибки в файл, не показывай пользователю!

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);
Named arguments делают код более читаемым и защищают от ошибок при изменении порядка параметров. Отлично подходят для конфигурационных функций!

Match Expression

Match выражение вместо switch
// Старый способ (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
Enums обеспечивают type-safe способ работать с ограниченным набором значений. Используй их вместо констант или строк для лучшей типобезопасности!

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 с ключами
// 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 (делегирование)

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 - Захват переменных

Closure и область видимости
$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);
};
use() захватывает значение в момент определения функции. Используй use(&$var) для захвата по ссылке, если нужны изменения!

Arrow Functions (PHP 7.4+)

Краткий синтаксис fn =>
// Старый способ (анонимная функция)
$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
Arrow functions идеальны для простых операций! Они автоматически захватывают переменные, поэтому не нужно use(). Но они могут содержать только ОДНО выражение!

Binding и $this в Closures

Closure и объекты
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

Closure для 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 — всё в одной таблице

Финальная распечатка перед проверкой. Все ключевые темы в виде таблиц с примерами. Используй как Anki-колоду или как референс «один взгляд — освежил память».

Типы данных и операторы сравнения

КонструкцияКак работаетПример
== (loose)Type juggling: приводит типы перед сравнением'0' == false → true
=== (strict)Сравнение типа И значения'1' === 1 → false
<=> (spaceship)Возвращает -1 / 0 / 11 <=> 2 → -1
?? (null coalescing)Значение если null/undefined$a ?? 'default'
?-> (nullsafe)Цепочка вызовов без NPE (PHP 8)$user?->profile?->avatar
declare(strict_types=1)Требует точный тип в аргументахКидает TypeError при несовпадении
(array)objectCast объекта в массивprivate → "\0ClassName\0prop" (null-байты!)
False-y значения PHP — ВСЁ что превращается в false
// 6 значений (все остальные — true):
false, 0, 0.0, "", "0", null, []

// Ловушки:
// "0.0"  → true (это строка не пустая!)
// "false" → true (это строка не пустая!)
// [0] → true (массив не пустой)
PHP 8.0 — ИЗМЕНЕНИЕ сравнения строка vs число (RFC)

Старое поведение (PHP < 8.0): при == между строкой и числом — строка приводилась к числу. Нечисловая строка → 0. Поэтому "abc" == 0 было true — источник классических уязвимостей.

Новое поведение (PHP 8.0+): если строка нечисловая — наоборот, число приводится к строке, сравниваются две строки. Если строка числовая ("10", "1e2") — по-прежнему сравниваются как числа.

ВыражениеPHP < 8.0PHP 8.0+Почему
"10" == 10truetrueСтрока числовая, сравнение как числа (не изменилось)
"10" === 10falsefalseStrict: string ≠ int (не изменилось)
"" == 0truefalseНечисловая (пустая) строка. PHP 8: 0 → "0", "" ≠ "0"
"abc" == 0truefalseНечисловая строка. PHP 8: 0 → "0", "abc" ≠ "0"
"0" == 0truetrueСтрока числовая (значение 0)
"1abc" == 1truefalse«Полу-числовая» — раньше парсилась до 1, в PHP 8: nonnumeric → сравнение строк
"100" == "1e2"truetrueОбе строки числовые: 100 == 100
'0' == falsetruetrueСравнение с bool — другие правила (приведение к bool)
Правило для шпаргалки (PHP 8.0+):
  • Строка ЧИСЛОВАЯ + число → сравнение как числа ("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Ключ есть И не nullbool—
Флаги array_filter
ФлагСигнатура callbackКогда применять
по умолчаниюfn($value)Стандартная фильтрация по значению
ARRAY_FILTER_USE_KEYfn($key)Фильтрация по ключу (префикс/суффикс ключа)
ARRAY_FILTER_USE_BOTHfn($value, $key)Когда условие зависит от обоих
array_reduce порядок аргументов callback: fn($carry, $item) — 1-й аккумулятор, 2-й текущий элемент. Имена НЕ важны, важен порядок. Это зашито в реализацию PHP.

ООП — abstract vs interface vs trait

ХарактеристикаAbstract ClassInterfaceTrait
Множественное использованиеодин родительсколько угодносколько угодно
Свойства (поля)любая видимостьтолько константылюбая видимость
Реализация методовмикс abstract+concreteтолько сигнатурыполные методы
Конструкторданеттехнически да, опасно
Создать инстанснетнеттолько через use
Type-hint в функцияхдада (рекомендовано)trait — не тип
Семантика«is-a»«can-do»«копипаст методов»
Когда выбиратьиерархия + общее состояниеконтракт для DI / полиморфизмакод для разных веток
Late Static Binding — static:: vs self::
КонструкцияРезолвится вИспользовать когда
self::Класс где написано (compile-time)Нужен именно ЭТОТ класс, без подмены
static::Реальный вызываемый класс (runtime)Полиморфизм через статику (фабрики, ORM)
parent::Родительский классВызов родителя из переопределённого метода

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

МетодКогда вызываетсяТипичный use case
__constructnew Class(...)Инициализация, RAII захват ресурса
__destruct$obj = null / выход из scopeRAII освобождение ресурса
__get($name)Чтение несуществующего/privateORM атрибуты, прокси
__set($name, $value)Запись несуществующего/privateORM атрибуты, валидация в сеттере
__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 $objValue Objects (Money → "$100")
__invoke($args)$obj() — объект как функцияSingle Action Classes (Laravel)
__clone()clone $objГлубокое клонирование (deep copy)
__debugInfo()var_dump / print_rСкрыть password, секреты из дампа
__serialize() / __unserializeserialize($obj)Контроль сериализации, защита secrets
RAII (Resource Acquisition Is Initialization): конструктор захватывает ресурс, деструктор освобождает. Применимо к: файлам, сокетам, блокировкам, транзакциям, GD-изображениям.

PHP 8.x — ключевые фичи

ФичаВерсияПримерЗачем
match8.0match($x) { 1, 2 => 'a' }Строгое сравнение (vs switch), возвращает значение
enum8.1enum Status { case Active; }Типизированные константы, методы
readonly8.1public readonly int $id;Иммутабельность (Value Objects, DTO)
Constructor promotion8.0__construct(private int $id)Меньше boilerplate в DTO
Named arguments8.0foo(name: 'Alice', age: 30)Читаемость, можно пропускать опц. параметры
?-> nullsafe8.0$user?->profile?->avatarЦепочка без NullPointerException
First-class callable8.1strlen(...)Передача функции как closure без обёртки
Attributes8.0#[Route('/path')]Декларативная конфигурация (Symfony Routes)
Fibers8.1new Fiber(fn() => ...)Корутины, основа Amphp/ReactPHP
Intersection types8.1Countable&Iterator $xМножественные требования к типу

Top-30 тем — что нужно знать

#ТемаУровеньРаздел KB
1== vs === (type juggling)Junior+1. Типы данных
2False-y значения (что превращается в false)Junior+1. Типы данных
3abstract vs interface vs traitMiddle5. ООП Abstract, 6. Traits
4self:: vs static:: (Late Static Binding)Middle4. ООП Основы
5RAII: __construct + __destructMiddle7. Магические методы
6array_map vs filter vs reduceJunior+3. Массивы
7& в array_walk — by referenceMiddle3. Массивы
8Lifecycle PHP-запроса (index.php → response)Middlecross-ref KB_3
9PSR-4 autoloadingMiddle8. Namespaces
10composer.json sectionsJunior+8. Namespaces
11Exception hierarchy (Error vs Exception)Middle9. Обработка ошибок
12Generators yield — когда, зачемMiddle11. Генераторы
13Closures: use() vs arrow fnMiddle12. Closures
14Магические методы — цена за магиюMiddle7. Магические методы
15PDO prepared statements (защита от SQLi)Junior+cross-ref KB_2/KB_4
16password_hash vs md5Junior+cross-ref KB_4
17Spread operator ... в массивах и аргументахMiddle3. Массивы
18Union types int|stringMiddle1. Типы данных
19Nullable ?intJunior+1. Типы данных
20Constructor promotion (PHP 8)Middle10. PHP 8+
21match vs switchMiddle10. PHP 8+
22enum (PHP 8.1)Middle10. PHP 8+
23readonly свойстваMiddle4. ООП Основы
24Attributes (PHP 8) — Laravel routes/validationSenior10. PHP 8+
25Fibers — корутиныSenior10. PHP 8+
26opcache / JITSeniorcross-ref KB_6
27Copy-on-Write для массивовSenior3. Массивы
28SplObjectStorage, WeakMapSenior—
29Iterator vs GeneratorMiddle11. Генераторы
30__invoke — Single Action ClassesMiddle7. Магические методы, cross-ref KB_5

Вопросник для самопроверки (PHP Core)

Реальные вопросы, которые закрывают уровень middle PHP / Laravel. Сначала отвечай вслух, потом кликни вопрос — увидишь развёрнутый ответ. Если запнулся — открой соответствующий раздел KB. Важен не дословный ответ, а понимание сути.

Уровень 1 — Junior+ · 10 вопросов

1 В чём разница == и ===? Приведи пример где результат отличается.

== (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-сравнение даёт неочевидные баги.

2 Что выведет: 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 осторожнее.

3 Что такое type juggling? Где он опасен?

Type juggling — автоматическое приведение типов при сравнении или операциях. PHP пытается «угадать» что ты хотел.

Опасен в трёх местах: 1) сравнения паролей — $hash == $userInput может магически совпасть с 0; используй hash_equals(). 2) in_array($needle, $haystack) без 3-го параметра true — найдёт "foo" если в массиве есть 0. 3) ключи массивов — числовые строки "5" и числа 5 резолвятся в один ключ.

4 Назови 6 false-y значений в PHP.

6 значений, которые в bool-контексте становятся false: false, 0, 0.0, "", "0", null, [] (пустой массив).

Ловушки: "0.0" → true (непустая строка!), "false" → true (строка), [0] → true (непустой массив).

5 В чём разница между isset и array_key_exists?

isset($arr[$key]) — true если ключ есть И значение не null.

array_key_exists($key, $arr) — true если ключ есть, даже если значение null.

Когда применять array_key_exists: если null — валидное значение в твоём массиве (например, кэш может закэшировать null как результат).

6 Что делает 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-кода».

7 В чём разница между fn (arrow) и function (anonymous)?

Arrow function fn($x) => $x*2 — короткий синтаксис, автоматически захватывает переменные из родительского scope по значению. Только одно выражение, без {}.

Anonymous function function($x) use ($y) { ... } — нужен явный use для захвата. Может содержать любое тело. По умолчанию захват по значению (по ссылке — use (&$y)).

8 Чем отличается private от protected?

private — доступно только в самом классе. Даже наследник не видит.

protected — доступно в классе и во всех наследниках.

На практике: используй private по умолчанию (инкапсуляция). protected — только когда осознанно проектируешь расширяемость через наследование.

9 Что такое $this? Когда нельзя использовать?

$this — ссылка на текущий экземпляр объекта внутри метода класса.

Нельзя использовать: в static методах (нет инстанса — есть только класс); в обычных функциях вне класса; в __destruct после уничтожения других полей.

В static-методе вместо $this — self:: (текущий класс) или static:: (Late Static Binding).

10 Что выведет 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 вопросов

11 В чём разница abstract class и interface? Когда что?

Abstract class — частичная реализация. Может иметь свойства, конкретные методы, абстрактные методы. Один родитель.

Interface — только контракт (сигнатуры). Свойств нет, только константы. Можно реализовать сколько угодно.

Когда: abstract — если есть общее состояние и «is-a» иерархия (BaseController); interface — если нужен контракт для DI / полиморфизма (PaymentInterface). На практике часто комбинируют: class Dog extends Animal implements Trainable.

12 Зачем нужен trait? Чем отличается от abstract class?

Trait — горизонтальное переиспользование кода. Класс может подключить сколько угодно трейтов через use. Решает проблему отсутствия множественного наследования.

Отличие от abstract: abstract — это «is-a» (Animal → Dog). Trait — это «has-behavior» (Loggable, Cacheable, Notifiable). Используй trait когда метод нужен в неродственных классах.

13 Может ли trait хранить свойства? А интерфейс? А abstract?

Abstract class: да, любые свойства любой видимости.

Interface: только константы (const), свойств нет.

Trait: да, любые свойства. При use попадают в класс как родные. Если в классе уже есть свойство с тем же именем — fatal error при несовместимом объявлении.

14 Что такое Late Static Binding? Чем 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() возвращает экземпляр конкретной модели.

15 Что делает final? А readonly?

final class — нельзя унаследоваться. final method — нельзя переопределить в наследниках.

readonly (PHP 8.1) — свойство можно проинициализировать ОДИН раз (в конструкторе), потом — fatal error при попытке записи. Применяется в Value Objects, DTO.

Разница: final запрещает переопределение, readonly запрещает изменение значения.

16 Назови 5 магических методов и когда они вызываются.

__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").

17 Что такое RAII? Приведи 3 примера применения __construct/__destruct.

RAII (Resource Acquisition Is Initialization) — паттерн из C++: конструктор захватывает ресурс, деструктор освобождает. Гарантирует освобождение даже при throw.

Примеры:

  • FileHandler — fopen в конструкторе, fclose в деструкторе.
  • TransactionGuard — beginTransaction → rollBack если не было commit().
  • FileLock — flock LOCK_EX → LOCK_UN. Блокировка снимется при выходе из scope.
18 Что происходит при $user->name = 'X', если в классе нет свойства $name?

PHP не находит свойство $name → проверяет, есть ли в классе __set().

Если __set есть → вызывается с аргументами ('name', 'X'). Без ошибки.

Если __set нет → создаётся динамическое публичное свойство $user->name = 'X'. В PHP 8.2+ — deprecated warning. В PHP 9 — будет fatal error без #[AllowDynamicProperties].

19 Что такое PSR-4? Зачем нужен autoload?

PSR-4 — стандарт autoloading. Маппит namespace на путь к файлу: App\Models\User → app/Models/User.php. Composer генерирует автозагрузчик.

Autoload избавляет от ручных require: PHP сам подгрузит файл при первом использовании класса. Без него — гора require в каждом файле.

Настройка в composer.json: "autoload": { "psr-4": { "App\\\\": "app/" } }.

20 Чем отличается require, require_once и autoload?

require — подгружает файл, fatal error если файла нет. Каждый раз заново.

require_once — подгружает только если ещё не подгружен (отслеживается на уровне PHP).

Autoload — PHP сам вызывает функцию (зарегистрированную через spl_autoload_register) когда видит неизвестный класс. Composer регистрирует PSR-4 автозагрузчик.

В современном коде require/require_once почти не пишут — всё через Composer autoload.

21 Объясни как работает 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-й — текущий элемент. Имена переменных не важны.

22 В чём разница array_map, array_filter, array_walk?

array_map($cb, $arr) — трансформирует, возвращает новый массив. Ключи сбрасываются для числовых массивов.

array_filter($arr, $cb) — фильтрует, возвращает новый массив. Ключи сохраняются (это частый сюрприз — после фильтрации могут быть «дырки» в индексах).

array_walk($arr, $cb) — мутирует массив in-place. Callback принимает &$value по ссылке. Возвращает bool.

23 Зачем нужен & в array_walk(&$item)?

& — передача по ссылке. Без неё callback получит копию значения и не сможет изменить элемент массива.

С & любые изменения $item внутри callback меняют сам массив (in-place mutation).

Контраст с array_map: map создаёт новый массив (без мутации), walk — мутирует исходный.

24 Что делает 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).

25 Что такое spread operator ... в массивах? В аргументах?

В аргументах: function sum(...$nums) — variadic, собирает аргументы в массив.

При вызове: sum(...[1,2,3]) — распаковывает массив в отдельные аргументы.

В массивах (PHP 7.4+): $merged = [...$arr1, ...$arr2, 'extra'] — расширение массивов. Быстрее чем array_merge.

С string-ключами (PHP 8.1+): [...$assoc1, ...$assoc2] — работает, дубликаты ключей перезаписываются справа.

26 Когда нужен generator (yield)? Чем отличается от обычной функции?

Generator — функция, которая «лениво» возвращает значения через yield вместо return. Не загружает весь массив в память.

Когда нужен: чтение больших файлов построчно (миллионы строк), обработка потоков, бесконечные последовательности.

Пример: чтение CSV — file_get_contents загружает 10 GB в память → OOM. Generator с fgets → 1 строка за раз, постоянное потребление памяти.

27 Что такое closure? Зачем use ($var)? Можно ли менять переменную в closure?

Closure — анонимная функция, которая «захватывает» переменные из внешнего scope.

use ($var) — явный список переменных для захвата. По умолчанию по значению (копия). Для захвата по ссылке: use (&$var).

Менять переменную: по значению — изменения внутри closure не видны снаружи. По ссылке — видны.

Arrow function (fn) автоматически захватывает всё нужное по значению, без use.

28 Объясни разницу между Error и Exception.

Оба наследуют Throwable, оба ловятся через catch.

Exception — бизнес/доменные ошибки (validation, not found, unauthorized). Их ловят и обрабатывают.

Error — системные/программные (TypeError, ParseError, OutOfMemoryError). Обычно НЕ ловят — это баг, чинить код, а не глотать.

Иерархия: Throwable ← Error / Exception. Чтобы поймать всё: catch (\Throwable $t).

29 Что такое match? Чем отличается от switch?

match (PHP 8) — выражение, возвращает значение. Использует строгое сравнение ===. Не нужен break. Если ни одна ветка не подошла — UnhandledMatchError.

switch — оператор (не выражение). Использует loose ==. Нужен break, иначе fall-through. Если ничего не подошло и нет default — молча идёт дальше.

В новом коде — match предпочтительнее: безопаснее, короче.

30 Зачем нужен enum (PHP 8.1)? Чем отличается от констант?

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 вопросов

31 Что такое attributes (PHP 8)? Приведи пример из Laravel/Symfony.

Attributes — нативный механизм декларативной метаинформации над классами/методами/свойствами. До PHP 8 жили в doc-комментариях как аннотации.

Пример Symfony: #[Route('/users/{id}', methods: ['GET'])] public function show(int $id) {...}.

Laravel: #[Route] в роутинге PHP 8 style, #[Validate('email')] в формах.

Читаются через рефлексию (ReflectionClass::getAttributes()).

32 Объясни fibers (PHP 8.1). В чём отличие от generators?

Fibers — корутины с явным управлением. Можно suspend в середине выполнения и resume снаружи.

Отличие от generators: generator может только yield вверх по стеку (своему caller). Fiber может suspend/resume произвольно — это основа для async-фреймворков (AmpHP, ReactPHP).

На практике: писать fibers напрямую почти не приходится — используют поверх async-библиотек.

33 Как работает opcache? Что такое JIT (PHP 8)?

opcache — кэширует скомпилированный bytecode PHP в shared memory. Без него каждый запрос парсит .php файлы заново.

JIT (Just-In-Time) — компиляция «горячего» bytecode в нативный машинный код. Включается в opcache (opcache.jit=tracing).

Эффект: на типичных веб-задачах (ввод/вывод) JIT даёт ~3-5%. На вычислительных (math, image processing) — до 2-3x.

34 Что такое weak references (WeakMap)? Зачем нужны?

WeakReference / WeakMap — хранят объект, но не мешают GC его удалить. Если объект больше нигде не используется — он удаляется, ссылка обнуляется.

Зачем: кэш по объектам, observers/listeners (чтобы listener не удерживал объект в памяти бесконечно), object identity maps в ORM.

До PHP 7.4 пришлось бы хранить ID + использовать обычный массив — но это утечка памяти.

35 Как PHP управляет памятью? Что такое circular reference?

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.

36 Объясни Copy-on-Write для массивов в PHP.

Когда массив присваивается другой переменной ($b = $a) — PHP не копирует данные сразу. Обе переменные ссылаются на одни данные с refcount=2.

Копия создаётся только при первой модификации одной из них ($b[] = 1). Это и есть Copy-on-Write.

Эффект: передача массива в функцию by-value — бесплатна, пока никто не пишет. Поэтому immutable-стиль в PHP не такой дорогой как кажется.

37 В чём проблема serialize объектов? Как её решает __serialize?

serialize($obj) сериализует все свойства объекта, включая приватные. Проблема: secrets / passwords / API tokens попадают в сериализованный вид. Если такой объект сохраняется в кэш — leak.

__serialize() (PHP 7.4+) — позволяет вернуть array с тем, что хочешь сохранить. Полный контроль.

__unserialize($data) — восстанавливает объект из этого массива.

38 Что такое SplObjectStorage? Зачем нужен?

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

В обычном массиве PHP ключ может быть только string/int — объект использовать нельзя. SplObjectStorage работает по spl_object_hash($obj).

Применение: object identity maps в ORM, observer registries, dependency graphs.

39 Как реализовать iterable через Iterator vs Generator?

Iterator — интерфейс с 5 методами: current(), key(), next(), rewind(), valid(). Полный контроль над итерацией. Можно несколько раз пройти.

Generator — функция с yield. Короче, но одноразовый: после прохождения нельзя rewind (бросает исключение).

В Laravel LazyCollection — обёртка над Generator с возможностью «обернуть в Iterator» при необходимости.

40 Что такое Stringable interface (PHP 8)?

Stringable — интерфейс с одним методом __toString(): string. Любой класс с __toString автоматически считается Stringable (это implicit interface).

Применение: type-hint function log(string|Stringable $msg) — принимает и строки, и объекты с __toString (например, Money, Url).

41 Объясни поведение static переменных в функциях.

function f() { static $count = 0; $count++; return $count; } — $count сохраняется между вызовами, инициализируется один раз.

Это не глобальная переменная — она видна только внутри функции. Полезно для счётчиков, мемоизации (хотя для серьёзной мемоизации лучше делать отдельный объект-кэш).

Не работает с recursion-эффектами в parallel testing — статика общая на процесс.

42 Что такое __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 для команды.

43 Чем отличается ::class от get_class($obj)?

User::class — резолвится в compile-time в полное имя класса с namespace ("App\\Models\\User"). Работает с use-импортами, IDE подсказывает.

get_class($obj) — резолвится в runtime, возвращает класс конкретного экземпляра. Без аргумента (внутри метода) — текущий класс.

Когда что: ::class — для типов в сигнатурах, биндингов DI, attributes. get_class — когда нужно знать реальный класс instance (например, для логирования).

44 В чём опасность extract() и compact()?

extract($arr) — создаёт переменные в текущем scope из ключей массива. Опасность: может перезаписать существующие переменные. Если массив от пользователя — security hole.

compact('a', 'b') — обратная операция: создаёт массив из переменных по их именам. Менее опасно, но IDE не подсказывает.

В современном коде их избегают: extract — почти никогда, compact — иногда в передаче в Blade-views (хотя Laravel рекомендует именованные массивы).

45 Как реализовать singleton без global state? Какие альтернативы?

Классический 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.

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

Практика руками (PHP Core)

Без практики теория не приклеивается. Здесь — задания трёх уровней: микро-задачи (15 мин) для разогрева, готовые задания (30-60 мин) с полным разбором и эталонным кодом, мини-проекты (1-2 часа) в стиле LeetCode. Каждое задание — Постановка → Шаги → Эталонный код → Тесты → Подводные камни. Не подсматривай решение до первой попытки.

Микро-задачи (15 мин каждая) — разогрев

Каждая задача с эталонным решением. Сначала пиши сам, потом сверяй.
Задача 1: isTruthy($x) ~15 мин · type juggling

Постановка: Функция возвращает true только если $x НЕ входит в 6 false-y значений PHP. Покрой 10 тестами.

Эталонное решение
declare(strict_types=1);

function isTruthy(mixed $x): bool
{
    // Самый простой и корректный способ — bool-cast
    return (bool) $x;
}
Тесты (PHPUnit)
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 точнее и короче.
Задача 2: strict_types и TypeError ~15 мин · типы

Постановка: Напиши функцию 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).
Задача 3: extractEmails($text) ~15 мин · regex

Постановка: Через 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, но в результат вернётся оригинальный регистр.
Задача 4: formatPrice($amount, $currency) ~15 мин · sprintf

Постановка: Форматирование цены в виде "$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"
Подводные камни: для локализации (русские числа: «1 234,56») — использовать NumberFormatter::CURRENCY из ext-intl. В JSON-API лучше отдавать цена + валюта отдельно (минимальные единицы — копейки/центы), а форматировать на фронте.
Задачи 5-10: микро-разогрев ~10 мин каждая

Постановка и эталонные one-liner'ы (сделай свой вариант, потом сверь):

5. array_map — массив пользователей → массив имён
$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');
6. array_filter с USE_KEY
$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]
7. array_reduce — статистика заказов
$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]
8. array_walk с & — 10% налог in-place
$prices = [100, 200, 300];
array_walk($prices, fn(&$p) => $p *= 1.1);
// [110, 220, 330] — массив изменён in-place
9. usort с spaceship — сортировка объектов
$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
10. Destructuring ассоциативного массива
$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 мин)

Задание 1: trait Cacheable с защитой от null ~30 мин

Постановка: Реализуй trait Cacheable со свойством $cache и методом remember($key, callable $cb). Acceptance: кэш работает даже когда callback возвращает null (большинство наивных реализаций ломаются именно тут).

Шаги
  1. Объяви trait, добавь private array $cache = [].
  2. Метод remember: проверь наличие ключа через array_key_exists (НЕ isset!).
  3. Если есть — верни закэшированное. Если нет — вызови callback, сохрани, верни.
  4. Подключи в UserService через use Cacheable.
  5. Напиши тест с 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);
        });
    }
}
Тесты — критический кейс null
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.
Задание 2: TransactionGuard через RAII ~45 мин

Постановка: Класс автоматически откатывает транзакцию, если не было явного commit(). Защита от забытого rollback при exception.

Шаги
  1. Класс принимает PDO в конструкторе. Сразу вызывает beginTransaction().
  2. Свойство $committed = false.
  3. Метод commit(): вызвать $pdo->commit(), поставить $committed = true.
  4. Деструктор: если !$committed и $pdo->inTransaction() — rollBack().
  5. Тест: внутри функции с 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();
}
Тест — auto-rollback при exception
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 сработал
}
Подводные камни: деструктор вызывается при GC — порядок не гарантирован. Если TransactionGuard ссылается на другие объекты, которые тоже на GC — могут быть проблемы. В long-running процессах лучше явный finally { $tx->rollBackIfNeeded(); }. Также проверка $db->inTransaction() важна — без неё rollBack на уже зафиксированной транзакции бросит exception.
Задание 3: Магический QueryBuilder через __call ~45 мин

Постановка: 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 хочется явных сигнатур.
Задание 4: PaymentInterface с двумя реализациями ~45 мин

Постановка: Интерфейс + 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 — учти на проектировании).
Задание 5: Abstract BaseRepository через PDO ~50 мин

Постановка: 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);
Подводные камни: имя таблицы в SQL через интерполяцию — НЕ через prepared params (PDO не позволяет биндить идентификаторы). Поэтому критично, чтобы table() возвращал литерал, а не user input. LIMIT в PDO с MySQL надо биндить через PARAM_INT — иначе lexer воспримет число как строку и SQL сломается.

Мини-проекты в стиле LeetCode (1-2 часа)

Mini Project 1: Свой my_array_reduce ~60 мин

Постановка: Реализуй 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;
}
Тесты — 5 кейсов
// 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);
Подводные камни: убедись что callback получает аккумулятор 1-м аргументом. Если перевернёшь — все assert упадут. Это и есть правильный ответ «как PHP понимает порядок»: зашито в реализации функции.
Mini Project 2: Свой Collection (immutable) ~90 мин

Постановка: Класс с методами 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-5: краткие постановки по 1-2 часа

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?» — ты будешь знать на собственном коде.