Skip to content

Latest commit

 

History

History
1745 lines (1506 loc) · 94.5 KB

File metadata and controls

1745 lines (1506 loc) · 94.5 KB

Основы PHP: ключевые концепции и подводные камни

Эта глава — компактный обзор основ PHP, но не учебник для новичков. Книга предполагает, что вы уже знакомы с базовыми конструкциями языка и хотите углубить понимание. Здесь мы разберём типичные ошибки, нюансы языка и вопросы, которые часто всплывают на технических интервью. Основной фокус — на «подводных камнях» и темах, которые разработчики нередко упускают из виду. Без лишней «воды», только практичные примеры и полезные инсайты.

Почему PHP? Это не только один из самых популярных языков для веб-разработки, но и инструмент, позволяющий быстро создавать крупные веб-приложения с минимальными усилиями по деплою. Благодаря простоте синтаксиса и мощной экосистеме, PHP остаётся востребованным для проектов любого масштаба.

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

PHP — динамически типизированный язык, что означает, что типы переменных определяются во время выполнения и могут меняться в зависимости от контекста. Однако его поведение с типами данных иногда вызывает путаницу из-за неявного преобразования типов. В PHP 8 поддерживаются следующие типы данных:

  • int: Целые числа, например, 42 или -17. Не имеют дробной части, размер зависит от платформы (обычно 32 или 64 бита).
  • float: Числа с плавающей точкой, например, 3.14 или -0.001. Используются для представления дробных чисел, могут терять точность при больших значениях.
  • string: Строки, последовательности символов, например, "hello" или 'world'. Могут быть однобайтовыми или многобайтовыми (например, для UTF-8).
  • bool: Логические значения, только true или false. Используются в условных выражениях.
  • array: Упорядоченные коллекции данных (массивы), которые могут содержать элементы любых типов, например, [1, "test", true]. Могут быть индексированными или ассоциативными.
  • object: Экземпляры классов, содержащие свойства и методы, например, new stdClass(). Используются для объектно-ориентированного программирования.
  • null: Специальный тип, обозначающий отсутствие значения. Переменная с null не имеет определённого значения.
  • resource: Специальный тип для хранения ссылок на внешние ресурсы, такие как открытые файлы или соединения с базой данных. Используется реже в современных версиях PHP.
  • callable: Тип, представляющий вызываемые функции, методы или замыкания, например, ['Class', 'method'] или анонимные функции.
  • mixed: Псевдотип, введённый в PHP 8, обозначающий любой тип данных. Используется в аннотациях типов для большей гибкости.
  • void: Псевдотип, указывающий, что функция ничего не возвращает. Используется только в объявлениях функций.
  • never: Псевдотип, введённый в PHP 8.1, указывает, что функция никогда не возвращает управление (например, из-за exit() или исключения).
  • iterable: Псевдотип, представляющий значения, которые можно перебирать в цикле foreach, такие как массивы или объекты, реализующие интерфейс Traversable.

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

Подводные камни

  • Сравнение с == и ===: Оператор == приводит типы перед сравнением, что может привести к неожиданным результатам. Используйте === для строгого сравнения (значение и тип).
    $a = "10";
    $b = 10;
    var_dump($a == $b);  // bool(true) — приводит строку к числу
    var_dump($a === $b); // bool(false) — разные типы
  • Числа и строки: Приведение строк к числам может быть неочевидным.
    $str = "123abc";
    var_dump((int)$str); // int(123) — PHP игнорирует нечисловые символы

Логические значения

Функции empty(), isset(), is_null(), оператор === null, и array_key_exists() используются для проверки переменных и элементов массивов, но их поведение имеет важные различия, которые часто приводят к ошибкам.

  • empty($var): Проверяет, является ли переменная «пустой». Считает «пустыми» значения: false, 0, 0.0, "", "0", null, [], и неинициализированные переменные.
  • isset($var): Проверяет, существует ли переменная и не равна ли она null.
  • is_null($var): Проверяет, равна ли переменная строго null. Выбрасывает E_WARNING, если переменная не определена.
  • === null: Строгое сравнение с null, проверяет, является ли значение переменной null. Также выбрасывает E_WARNING для неопределённой переменной.
  • array_key_exists($key, $array): Проверяет, существует ли указанный ключ в массиве.

Пример с переменной

$var = "0";
$nullVar = null;
$unsetVar;

var_dump(empty($var));        // bool(true) — строка "0" считается пустой
var_dump(isset($var));        // bool(true) — переменная существует
var_dump(is_null($var));      // bool(false) — переменная не null
var_dump($var === null);      // bool(false) — переменная не null
var_dump(empty($nullVar));    // bool(true) — null считается пустым
var_dump(isset($nullVar));    // bool(false) — null не проходит проверку isset
var_dump(is_null($nullVar));  // bool(true) — явно null
var_dump($nullVar === null);  // bool(true) — явно null
var_dump(empty($unsetVar));   // bool(true) — неинициализированная переменная пуста
var_dump(isset($unsetVar));   // bool(false) — неинициализированная переменная не существует
var_dump(is_null($unsetVar)); // bool(true) — неинициализированная переменная считается null
var_dump($unsetVar === null); // bool(true) — неинициализированная переменная равна null

Пример с массивом

$array = ["key" => "value", "empty" => "", "zero" => 0, "null" => null];

var_dump(array_key_exists("key", $array));   // bool(true) — ключ существует
var_dump(array_key_exists("missing", $array)); // bool(false) — ключ отсутствует
var_dump(isset($array["null"]));       // bool(false) — значение null не проходит isset
var_dump(empty($array["zero"]));       // bool(true) — значение 0 считается пустым
var_dump(is_null($array["null"]));     // bool(true) — значение явно null
var_dump($array["null"] === null);     // bool(true) — значение равно null

Подводные камни

  • empty() возвращает true для строки "0", что может быть неожиданным, если "0" — валидное значение.
  • isset() возвращает false для null, даже если ключ существует в массиве.
  • is_null() и === null ведут себя одинаково для существующих переменных, но оба вызывают E_WARNING для неопределённых переменных, в отличие от isset().
    var_dump(is_null($undefined)); // bool(true) + E_WARNING
    var_dump($undefined === null); // bool(true) + E_WARNING
    var_dump(isset($undefined));   // bool(false) — без предупреждения
  • array_key_exists() проверяет только наличие ключа, игнорируя значение, в отличие от isset().

Сравнение is_null() и === null

  • Поведение: Оба проверяют, является ли значение строго null. Для определённых переменных они идентичны по результату.
    $var = null;
    var_dump(is_null($var));  // bool(true)
    var_dump($var === null);  // bool(true)
  • Различия:
    • is_null() — функция, явно предназначенная для проверки null. Более читаема в сложных условиях.
    • === null — оператор сравнения, менее очевидный в коде, особенно при сравнении с другими значениями.
    • Для неопределённых переменных оба вызывают E_WARNING, но isset() позволяет избежать предупреждения.
  • Когда использовать:
    • Используйте is_null() для явной проверки на null (например, для читаемости).
    • Используйте === null в сложных условиях:
      if ($var === null || $var === 0) {
          echo "Переменная либо null, либо 0";
      }
    • Для проверки существования перед проверкой на null используйте isset():
      if (isset($var) && $var === null) {
          echo "Переменная существует и равна null";
      }

Производительность

Тестирование показывает, что isset() — самая быстрая функция для проверки существования переменной или ключа, так как выполняется на уровне движка PHP. === null быстрее, чем is_null(), так как это оператор. is_null() медленнее из-за накладных расходов на вызов функции. empty() медленнее всех, так как проверяет существование и «пустоту». array_key_exists() — самый медленный, так как работает с хэш-таблицей массива.

Примерные результаты производительности (на 1 млн итераций, PHP 8.1, зависят от окружения):

  • isset: ~0.03 сек
  • === null: ~0.04 сек
  • is_null: ~0.05 сек
  • empty: ~0.07 сек
  • array_key_exists: ~0.09 сек

Совет: Используйте isset() для проверки существования переменных или ключей, чтобы избежать предупреждений. Для проверки на null предпочтите === null в простых случаях из-за скорости, а is_null() — для читаемости. Избегайте empty(), если строка "0" — валидное значение. На собеседованиях часто спрашивают разницу между is_null(), === null, и isset(), а также их влияние на производительность.

strict_types — строгость типов

В PHP по умолчанию используется автоматическое приведение типов (type juggling), что может привести к неожиданным результатам и ошибкам, которые сложно отследить. Директива strict_types помогает сделать код предсказуемым, но действует только на вызовы функций и методов (проверка аргументов и возвращаемых типов).

Включение строгой типизации

<?php
declare(strict_types=1);

🔺 Это объявление должно быть первой строкой файла, до любого вывода или кода. Оно влияет только на текущий файл и не распространяется на включённые файлы (например, через require).

Пример: сравнение поведения

Без strict_types

function sum(int $a, int $b): int {
    return $a + $b;
}

echo sum(2, 3.5); // 5 — float приводится к int

Включив strict_types=1

declare(strict_types=1);

function sum(int $a, int $b): int {
    return $a + $b;
}

echo sum(2, 3.5); // ❌ Fatal error: Argument must be of type int

Важный нюанс

Строгая типизация с strict_types=1 применяется только к вызовам функций и методов:

  • Проверяются типы аргументов и возвращаемых значений.
  • Не влияет на операции внутри функций, присваивания или другие части кода. Например:
    declare(strict_types=1);
    $a = "123"; // Это строка
    $b = $a + 1; // Слабое приведение: результат 124

Проблемы при отключённой типизации

  • Тихое приведение типов (floatint, stringint и т.д.).
  • Логические ошибки, которые трудно отследить.
  • Непредсказуемое поведение, зависящее от входных данных.

Почему стоит всегда использовать strict_types

  • Защищает от скрытых ошибок при передаче аргументов или возврате значений.
  • Делает типы прозрачными и предсказуемыми.
  • Упрощает написание юнит-тестов.
  • Обеспечивает совместимость с современными стандартами и инструментами статического анализа (PHPStan, Psalm).

Рекомендуемый подход

  • Всегда включайте strict_types=1 в каждом PHP-файле, особенно в библиотеках или API.
  • Явно указывайте типы параметров и возвращаемых значений (int, string, array, bool, и т.д.).
  • Используйте в связке с авто-тестами и статическим анализом для максимальной надёжности.

Ссылки и переменные переменных

Ссылки и переменные переменных в PHP — мощные, но неочевидные инструменты, которые могут упростить код или усложнить отладку. В PHP 8 ссылки (&) позволяют передавать переменные, не создавая их копий в памяти (ссылаться одну область памяти), а переменные переменных ($$var) позволяют динамически обращаться к переменным. Этот раздел разбирает их применение, подводные камни.

Ссылки в PHP 8

Ссылки позволяют нескольким переменным указывать на одну и ту же область памяти. Оператор & используется для создания ссылок при передаче в функции, в циклах или при присваивании.

Использование ссылок

  • Передача в функции: Позволяет функции изменять исходную переменную.
    function increment(&$number) {
        $number++;
    }
    $value = 5;
    increment($value);
    var_dump($value); // int(6)
  • Циклы foreach: Ссылка на элемент массива позволяет изменять массив напрямую.
    $array = [1, 2, 3];
    foreach ($array as &$item) {
        $item *= 2;
    }
    var_dump($array); // [2, 4, 6]
  • Присваивание по ссылке: Создаёт псевдоним для переменной.
    $a = 10;
    $b = &$a;
    $b = 20;
    var_dump($a); // int(20) — $a и $b указывают на одну память

Работа с объектами

В PHP 8 объекты по умолчанию передаются по ссылке (точнее, по дескриптору объекта), поэтому & для объектов в функциях не требуется.

class Counter {
    public int $value = 0;
}

function incrementCounter(Counter $counter) {
    $counter->value++;
}

$counter = new Counter();
incrementCounter($counter);
var_dump($counter->value); // int(1)

Рекомендация: Избегайте & для объектов, так как это избыточно и может запутать код. Однако & полезен, если нужно заменить объект целиком.

function replaceCounter(&$counter) {
    $counter = new Counter();
}

$counter = new Counter();
replaceCounter($counter);
var_dump($counter); // object(Counter)#2 — новый объект

Клонирование объекта

$c = $a; // новый объект не создается; $c - это ссылка на тот же объект, что и $a
$b = clone $a; // создаётся копия объекта $a

Внутри класса объекта можно определить __clone() для модификации поведения при клонировании.

Подводные камни

  • Ошибки в foreach: После цикла с & переменная $item остаётся ссылкой, что может привести к неожиданным изменениям.
    $array = [1, 2, 3];
    foreach ($array as &$item) {
        $item *= 2;
    }
    $item = 10; // Изменяет последний элемент массива!
    var_dump($array); // [2, 4, 10]
    Решение: Сбрасывайте ссылку с помощью unset($item) после цикла.
    foreach ($array as &$item) {
        $item *= 2;
    }
    unset($item); // Безопасно
  • Копии вместо ссылок: Присваивание без & создаёт копию, что может быть неожиданным.
    $a = [1, 2];
    $b = $a; // Копия
    $b[0] = 10;
    var_dump($a); // [1, 2] — $a не изменился
  • Производительность: Ссылки могут замедлить выполнение из-за механизма copy-on-write в PHP.
    $array = range(1, 1000);
    $start = microtime(true);
    foreach ($array as &$item) {
        $item += 1;
    }
    echo "With reference: " . (microtime(true) - $start) . " seconds\n";
    
    $array = range(1, 1000);
    $start = microtime(true);
    foreach ($array as $key => $item) {
        $array[$key] += 1;
    }
    echo "Without reference: " . (microtime(true) - $start) . " seconds\n";
    Результаты (PHP 8.1, ориентировочно):
    • With reference: ~0.0003 сек
    • Without reference: ~0.0002 сек

Рекомендация: Используйте ссылки только при необходимости (например, для изменения данных). Для больших массивов работа без & может быть быстрее из-за оптимизации copy-on-write.

Переменные переменных

Переменные переменных ($$var) позволяют обращаться к переменной, имя которой хранится в другой переменной. Это полезно для динамического доступа к данным.

Пример использования

$name = "price";
$$name = 100; // Создаёт переменную $price
var_dump($price); // int(100)

Практический пример: Динамическая обработка конфигурации.

$config = ['user' => 'Alice', 'role' => 'admin'];
foreach ($config as $key => $value) {
    $$key = $value;
}
var_dump($user, $role); // string(5) "Alice", string(5) "admin"

Подводные камни

  • Читаемость: Код с $$var трудно читать и отлаживать.
    $var = "x";
    $x = 10;
    $$var = 20;
    var_dump($x); // int(20) — неочевидно
  • Безопасность: Динамическое создание переменных может привести к уязвимостям, если имена берутся из пользовательского ввода.
    $var = $_GET['name']; // Опасно!
    $$var = 1; // Может перезаписать критические переменные
    Решение: Используйте массивы вместо "переменных переменных".
    $config = [];
    $config[$_GET['name']] = 1; // Безопаснее
  • Производительность: Доступ через $$var медленнее, чем через массив.
    $iterations = 1000000;
    $name = "price";
    
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $$name = 100;
    }
    echo "Variable variable: " . (microtime(true) - $start) . " seconds\n";
    
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $data[$name] = 100;
    }
    echo "Array access: " . (microtime(true) - $start) . " seconds\n";
    Результаты (PHP 8.1, ориентировочно):
    • Variable variable: ~0.05 сек
    • Array access: ~0.03 сек

Рекомендация: Заменяйте переменные переменных ассоциативными массивами для лучшей читаемости и безопасности. Используйте $$var только в редких случаях, например, для динамической конфигурации.

Советы

  • Используйте & для изменения данных в функциях или циклах, но избегайте для объектов, так как они передаются по дескриптору.
  • Сбрасывайте ссылки после foreach с unset() для избежания ошибок.
  • Заменяйте переменные переменных ($$var) массивами для безопасности и скорости.
  • На собеседованиях часто спрашивают:
    • Разницу между передачей по значению и по ссылке.
    • Почему foreach с & может привести к ошибкам.
    • Как работают переменные переменных и их риски.

Строки и кодировки

Строки в PHP — один из основных типов данных, но работа с ними, особенно с многобайтными кодировками (UTF-8), полна подводных камней. Этот раздел разбирает функции для работы со строками, кодировки и типичные ошибки.

Ключевые моменты

  • Однобайтные vs многобайтные строки: PHP по умолчанию считает строки однобайтными, что ломает работу с UTF-8. Используйте функции mb_* для корректной обработки.
    $str = "Привет";
    var_dump(strlen($str));     // int(12) — считает байты
    var_dump(mb_strlen($str)); // int(6) — считает символы (UTF-8)
  • Кодировка по умолчанию: PHP 8 использует UTF-8 для новых функций, но старые (например, htmlspecialchars()) требуют явного указания кодировки.
    $str = "Тест <b>";
    echo htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); // Тест &lt;b&gt;

Типичные ошибки

  • Обрезка строк: substr() режет по байтам, что может «сломать» UTF-8 символы.
    $str = "Привет";
    var_dump(substr($str, 0, 3));     // string(3) — обрезает некорректно
    var_dump(mb_substr($str, 0, 3)); // string(6) "При" — корректно
  • Регулярные выражения: Без флага u (PCRE_UTF8) Unicode-символы обрабатываются неправильно.
    $str = "Привет, мир!";
    var_dump(preg_match('/\w+/', $str));      // int(0) — не распознаёт кириллицу
    var_dump(preg_match('/\w+/u', $str));    // int(1) — корректно с UTF-8
  • Кодировка ввода/вывода: Если сервер или БД используют разные кодировки (например, Windows-1251), вывод может быть искажён.
    $str = mb_convert_encoding("Привет", "Windows-1251", "UTF-8");
    var_dump($str); // строка в Windows-1251

Производительность

Функции mb_* медленнее стандартных (strlen, substr) из-за обработки многобайтных символов.

$str = "Привет, мир!";
$iterations = 1000000;

$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    strlen($str);
}
echo "strlen: " . (microtime(true) - $start) . " seconds\n";

$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    mb_strlen($str);
}
echo "mb_strlen: " . (microtime(true) - $start) . " seconds\n";

Результаты (PHP 8.1, ориентировочно):

  • strlen: ~0.03 сек
  • mb_strlen: ~0.08 сек

Подводные камни

  • Неправильная кодировка: Если не указать UTF-8 в htmlspecialchars() или json_encode(), результат может быть некорректным.
    $data = ["text" => "Привет"];
    echo json_encode($data, JSON_UNESCAPED_UNICODE); // {"text":"Привет"}
    echo json_encode($data); // {"text":"\u041f\u0440\u0438\u0432\u0435\u0442"}
  • Производительность: Частое использование mb_* в циклах замедляет код. Кэшируйте результаты, если возможно.
  • Сравнение строк: Функции strcmp() или === не учитывают регистр или нормализацию Unicode.
    $str1 = "ё";
    $str2 = "ё"; // Комбинированный символ
    var_dump($str1 === $str2); // bool(false)

Рекомендации

  • Всегда используйте mb_* для работы с UTF-8 строками (включите расширение mbstring).
  • Указывайте кодировку явно в функциях вроде htmlspecialchars() и json_encode().
  • Для сложных операций с Unicode используйте библиотеки, такие как intl (например, Normalizer).

Подготовка к собеседованию

  • Часто спрашивают:
    • Разницу между strlen() и mb_strlen(). Ответ: strlen() — байты, mb_strlen() — символы (мультибайтовая поддержка).
    • Как обрабатывать UTF-8 в регулярных выражениях. Ответ: Добавить флаг u: preg_match('/\p{L}/u', $str).
    • Почему substr() может «сломать» строку. Ответ: режет по байтам, может обрезать мультибайтовый символ (UTF-8).
  • Подготовьтесь объяснить, как кодировки влияют на ввод/вывод и как избежать «кракозябры» в веб-приложениях.

Области видимости и замыкания

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

Ключевые моменты

  • Глобальная область: Переменные, объявленные вне функций, не доступны внутри без global или $GLOBALS.
    $x = 10;
    function test() {
        global $x;
        echo $x; // 10
    }
    test();
  • Статические переменные: Сохраняют значение между вызовами функции.
    function counter() {
        static $count = 0;
        return ++$count;
    }
    echo counter(); // 1
    echo counter(); // 2
  • Замыкания: Анонимные функции могут захватывать переменные с помощью use, но по умолчанию они передаются по значению.
    $x = 10;
    $closure = function() use ($x) {
        return $x;
    };
    $x = 20;
    echo $closure(); // 10 — захвачено исходное значение

Подводные камни

  • Изменение захваченной переменной в замыкании требует передачи по ссылке (use (&$x)).
    $x = 10;
    $closure = function() use (&$x) {
        $x++;
    };
    $closure();
    echo $x; // 11

Совет: Избегайте global, используйте параметры функций или замыкания для передачи данных. На собеседованиях часто спрашивают, как работает use в замыканиях.

Массивы и их особенности

Массивы в PHP — мощный инструмент, поддерживающий числовые и ассоциативные ключи, но их поведение может быть неочевидным. Неправильное использование массивов или функций для работы с ними часто приводит к ошибкам, особенно у начинающих разработчиков. Этот раздел разбирает типичные ошибки и функции, в которых путаются, а также даёт советы для собеседований.

Ключевые моменты

  • Числовые и ассоциативные массивы: PHP автоматически присваивает числовые ключи, но их можно смешивать со строковыми, что иногда вызывает путаницу.
    $array = [1, 2, "key" => "value"];
    var_dump($array); // [0 => 1, 1 => 2, "key" => "value"]
  • Операторы объединения: Оператор + и функция array_merge() ведут себя по-разному.
    $a = ["a" => 1, "b" => 2];
    $b = ["b" => 3, "c" => 4];
    $result = $a + $b; // ["a" => 1, "b" => 2, "c" => 4] — сохраняет первые значения ключей
    $merged = array_merge($a, $b); // ["a" => 1, "b" => 3, "c" => 4] — перезаписывает ключи
  • Удаление элементов: unset() не переиндексирует числовые ключи, что может нарушить логику.
    $array = [0, 1, 2];
    unset($array[1]);
    var_dump($array); // [0 => 0, 2 => 2]
    var_dump(array_values($array)); // [0 => 0, 1 => 2] — переиндексация

Типичные ошибки

  • Смешивание ключей: Числовые и строковые ключи могут конфликтовать, особенно если строка интерпретируется как число.
    $array = [10, "1" => 20];
    var_dump($array); // [0 => 10, 1 => 20] — ключ "1" интерпретируется как числовой
  • Неверное использование unset в циклах: Удаление элементов в цикле foreach может привести к пропуску элементов.
    $array = [1, 2, 3];
    foreach ($array as $key => $value) {
        unset($array[$key]);
    }
    var_dump($array); // [] — массив очищен
    Ошибка: Если использовать foreach с изменением массива, итератор может пропустить элементы. Используйте array_filter или цикл for для безопасного удаления.
    $array = [1, 2, 3];
    $array = array_filter($array, fn($value) => $value != 2);
    var_dump($array); // [0 => 1, 2 => 3]
  • Неправильная проверка существования: Использование isset() вместо array_key_exists() для проверки ключей с null значением.
    $array = ["key" => null];
    var_dump(isset($array["key"])); // bool(false) — не проходит для null
    var_dump(array_key_exists("key", $array)); // bool(true) — ключ существует

Функции, в которых путаются

  • array_merge(): Объединяет массивы, перезаписывая значения одинаковых ключей. Для числовых ключей создаёт новые индексы.
    $a = [1, 2];
    $b = [3, 4];
    var_dump(array_merge($a, $b)); // [0 => 1, 1 => 2, 2 => 3, 3 => 4]
  • array_column(): Извлекает значения определённого ключа из массива записей. Полезно для обработки данных из БД.
    $users = [
        ["id" => 1, "name" => "Alice"],
        ["id" => 2, "name" => "Bob"]
    ];
    $names = array_column($users, "name");
    var_dump($names); // ["Alice", "Bob"]
    Подводный камень: Если ключ отсутствует в записи, array_column() пропустит эту запись.
    $users = [
        ["id" => 1],
        ["name" => "Bob"]
    ];
    var_dump(array_column($users, "id")); // [1] — запись без ключа "id" игнорируется
  • array_combine(): Создаёт массив, используя один массив как ключи, а другой как значения.
    $keys = ["id1", "id2"];
    $values = ["Alice", "Bob"];
    var_dump(array_combine($keys, $values)); // ["id1" => "Alice", "id2" => "Bob"]
    Пример с БД: Создание словаря ID → имя из результата запроса.
    $users = [
        ["id" => 1, "name" => "Alice"],
        ["id" => 2, "name" => "Bob"]
    ];
    $idToName = array_combine(
        array_column($users, "id"),
        array_column($users, "name")
    );
    var_dump($idToName); // [1 => "Alice", 2 => "Bob"]
    Подводный камень: Оба массива должны иметь одинаковую длину, иначе ошибка.
    $keys = ["a", "b"];
    $values = [1];
    var_dump(array_combine($keys, $values)); // ValueError (PHP 8+)
  • array_key_exists(): Проверяет наличие ключа в массиве, даже если значение null.
    $array = ["key" => null];
    var_dump(array_key_exists("key", $array)); // bool(true)
    Подводный камень: Медленнее, чем isset(), но необходим для проверки ключей с null.
  • in_array(): Проверяет наличие значения в массиве.
    $array = [1, "2", 3];
    var_dump(in_array("2", $array)); // bool(true) — приводит типы
    var_dump(in_array("2", $array, true)); // bool(true) — строгое сравнение
    Подводный камень: Без третьего параметра (strict = true) выполняется приведение типов, что может дать ложный результат.
    var_dump(in_array("2", [2])); // bool(true) — приводит строку к числу
  • array_search(): Возвращает ключ первого найденного значения или false, если значение не найдено.
    $array = ["a" => 1, "b" => 2, "c" => 1];
    var_dump(array_search(1, $array)); // string(1) "a" — первый ключ
    Подводный камень: Без строгого сравнения (strict = true) может вернуть неверный ключ из-за приведения типов.
    var_dump(array_search("2", [2])); // int(0) — приводит типы
    var_dump(array_search("2", [2], true)); // bool(false) — строгое сравнение
  • array_map(): Применяет функцию к каждому элементу массива, возвращая новый массив.
    $array = [1, 2, 3];
    $result = array_map(fn($value) => $value * 2, $array);
    var_dump($result); // [2, 4, 6]
    Сравнение с foreach:
    • array_map(): Функциональный стиль, возвращает новый массив, не изменяет исходный.
      $array = [1, 2, 3];
      $result = array_map(fn($value) => $value + 1, $array);
      var_dump($result); // [2, 3, 4]
      var_dump($array); // [1, 2, 3] — исходный массив неизменён
    • foreach: Императивный стиль, может изменять исходный массив.
      $array = [1, 2, 3];
      foreach ($array as &$value) {
          $value += 1;
      }
      var_dump($array); // [2, 3, 4] — исходный массив изменён
    Подводный камень: array_map() не может изменять исходный массив напрямую и требует возврата нового массива. foreach удобнее для модификации массива, но менее читаем в функциональном стиле. Производительность: array_map() обычно медленнее foreach из-за вызова функции для каждого элемента.

    Кроме того, foreach — проще для чтения. Так что я не люблю array_map, хотя встречал отдельных деятелей, которые на код-ревью просили переписать foreach на array_map. Осуждаю, не будьте такими!

Подводные камни

  • Пустота массива: empty($array) возвращает true только для пустого массива, но [0] или [""] не считаются пустыми.
    $array = [0];
    var_dump(empty($array)); // bool(false)
  • Передача по ссылке: Изменение массива в функции требует явной передачи по ссылке.
    function modifyArray($array) {
        $array[] = 4; // Не изменяет исходный массив
    }
    $array = [1, 2, 3];
    modifyArray($array);
    var_dump($array); // [1, 2, 3]
    Решение: Используйте & для передачи по ссылке.
    function modifyArray(&$array) {
        $array[] = 4;
    }
    modifyArray($array);
    var_dump($array); // [1, 2, 3, 4]
  • Авто-инкремент ключей: PHP автоматически увеличивает числовые ключи, что может нарушить логику.
    $array = [0 => "a", 2 => "b"];
    $array[] = "c";
    var_dump($array); // [0 => "a", 2 => "b", 3 => "c"] — следующий числовой ключ

Производительность

Функции работы с массивами, такие как array_key_exists(), in_array(), array_search(), медленнее, чем isset(), из-за работы с хэш-таблицами. array_merge() быстрее, чем +, для больших массивов. array_map() медленнее foreach из-за накладных расходов на вызов функции.

Примерные результаты теста производительности (1 млн итераций, PHP 8.1, зависит от окружения):

  • isset: ~0.03 сек
  • array_key_exists: ~0.09 сек
  • in_array: ~0.11 сек
  • array_search: ~0.12 сек
  • array_map: ~0.15 сек
  • foreach: ~0.10 сек

Советы

  • Используйте array_merge() вместо + для объединения массивов, чтобы избежать потери данных.
  • Проверяйте ключи с array_key_exists(), если значение может быть null; для скорости используйте isset().
  • Для поиска значений указывайте strict = true в in_array() и array_search(), чтобы избежать приведения типов.
  • Используйте array_map() для функционального стиля, а foreach — для изменения массива или большей скорости.
  • На собеседованиях часто спрашивают:
    • Разницу между array_merge() и +.
    • Почему unset в foreach опасен.
    • Разницу между isset() и array_key_exists().
    • Как работают array_map() и array_column().

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

PHP предоставляет набор встроенных функций для сортировки массивов, которые оптимизированы на уровне движка и используют алгоритм introsort (Introspective Sort - гибрид быстрой сортировки, пирамидальной сортировки и сортировки вставками). Писать собственную сортировку редко оправдано, но полезно понимать, как работают стандартные функции и их производительность.

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

  • sort(): Сортирует массив по значениям, сбрасывая ключи (числовая индексация с 0).
    $array = [3, 1, 2];
    sort($array);
    var_dump($array); // [0 => 1, 1 => 2, 2 => 3]
  • rsort(): Сортирует по значениям в обратном порядке, сбрасывая ключи.
    $array = [3, 1, 2];
    rsort($array);
    var_dump($array); // [0 => 3, 1 => 2, 2 => 1]
  • asort(): Сортирует по значениям, сохраняя ключи.
    $array = ["b" => 2, "a" => 1];
    asort($array);
    var_dump($array); // ["a" => 1, "b" => 2]
  • arsort(): Сортирует по значениям в обратном порядке, сохраняя ключи.
    $array = ["b" => 2, "a" => 1];
    arsort($array);
    var_dump($array); // ["b" => 2, "a" => 1]
  • ksort(): Сортирует по ключам.
    $array = ["b" => 2, "a" => 1];
    ksort($array);
    var_dump($array); // ["a" => 1, "b" => 2]
  • krsort(): Сортирует по ключам в обратном порядке.
    $array = ["b" => 2, "a" => 1];
    krsort($array);
    var_dump($array); // ["b" => 2, "a" => 1]
  • usort(): Сортирует с пользовательской функцией сравнения, сбрасывая ключи.
    $array = [3, 1, 2];
    usort($array, fn($a, $b) => $a <=> $b);
    var_dump($array); // [0 => 1, 1 => 2, 2 => 3]
  • uasort(): Сортирует с пользовательской функцией, сохраняя ключи.
    $array = ["b" => 2, "a" => 1];
    uasort($array, fn($a, $b) => $a <=> $b);
    var_dump($array); // ["a" => 1, "b" => 2]
  • uksort(): Сортирует по ключам с пользовательской функцией.
    $array = ["b" => 2, "a" => 1];
    uksort($array, fn($a, $b) => $a <=> $b);
    var_dump($array); // ["a" => 1, "b" => 2]

Подводные камни

  • Сброс ключей: sort(), rsort(), и usort() сбрасывают ключи, что может нарушить ассоциативные массивы.
    $array = ["a" => 3, "b" => 1];
    sort($array);
    var_dump($array); // [0 => 1, 1 => 3] — ключи потеряны
  • Стабильность: В PHP сортировки нестабильны (одинаковые элементы могут менять порядок). Для стабильной сортировки требуется usort() с дополнительной логикой.
    $array = [["value" => 1, "id" => 1], ["value" => 1, "id" => 2]];
    usort($array, fn($a, $b) => $a["value"] <=> $b["value"]);
    var_dump($array); // Порядок id может измениться
  • Производительность usort(): Пользовательская функция сравнения замедляет сортировку из-за накладных расходов на вызовы.
    $array = [3, 1, 2];
    usort($array, fn($a, $b) => $a <=> $b); // Медленнее, чем sort()

Писать собственную сортировку (например, быструю сортировку) редко оправдано, так как встроенные функции PHP используют оптимизированный алгоритм introsort, реализованный на C.

Пример пользовательской быстрой сортировки:

function quickSort(&$array, $left, $right) {
    if ($left < $right) {
        $pivotIndex = partition($array, $left, $right);
        quickSort($array, $left, $pivotIndex - 1);
        quickSort($array, $pivotIndex + 1, $right);
    }
}

function partition(&$array, $left, $right) {
    $pivot = $array[$right];
    $i = $left - 1;
    for ($j = $left; $j < $right; $j++) {
        if ($array[$j] <= $pivot) {
            $i++;
            [$array[$i], $array[$j]] = [$array[$j], $array[$i]];
        }
    }
    [$array[$i + 1], $array[$right]] = [$array[$right], $array[$i + 1]];
    return $i + 1;
}

$array = [3, 1, 2];
quickSort($array, 0, count($array) - 1);
var_dump($array); // [1, 2, 3]

Поиск по массиву

Бинарный поиск в отсортированном массиве: Самый быстрый бинарный поиск применим только после сортировки массива. Стандартные функции (in_array(), array_search()) не используют бинарный поиск, так как массивы не гарантированно отсортированы => пользовательский бинарный поиск быстрее для больших отсортированных массивов.

function binarySearch($array, $value) {
    $left = 0;
    $right = count($array) - 1;
    while ($left <= $right) {
        $mid = (int)(($left + $right) / 2);
        if ($array[$mid] === $value) {
            return $mid;
        }
        if ($array[$mid] < $value) {
            $left = $mid + 1;
        } else {
            $right = $mid - 1;
        }
    }
    return false;
}

$array = [1, 2, 3, 4, 5];
sort($array); // Необходимо отсортировать
var_dump(binarySearch($array, 3)); // int(2)

Сравнение производительности

  • Стандартные функции: Используют introsort (O(n log n) в среднем), оптимизированный на C. Например, sort() быстрее пользовательской быстрой сортировки.
  • Пользовательская быстрая сортировка: O(n log n) обычно медленнее из-за интерпретируемого кода PHP.
  • Бинарный поиск: O(log n) для поиска в отсортированном массиве, быстрее, чем array_search() (O(n)), но требует предварительной сортировки (O(n log n)).

Примерные результаты теста производительности (на 10,000 элементов, PHP 8.1, зависит от окружения):

  • sort: ~0.002 сек
  • quickSort: ~0.15 сек
  • binarySearch (1000 searches): ~0.001 сек
  • array_search (1000 searches): ~0.05 сек

Выводы:

  • Стандартные функции (sort(), asort(), и т.д.) значительно быстрее пользовательской сортировки благодаря оптимизации на C.
  • Пользовательская быстрая сортировка в PHP медленнее (в ~75 раз в тесте) из-за интерпретируемого кода.
  • Бинарный поиск быстрее array_search() для больших отсортированных массивов, но требует предварительной сортировки.

Когда использовать

  • Стандартные функции: Используйте sort(), asort(), ksort() и их варианты для большинства задач, так как они быстрые, стабильные и простые.
  • Пользовательская сортировка: Применяйте только в редких случаях, когда требуется специфическая логика, не поддерживаемая usort().
  • Бинарный поиск: Полезен для частых поисков в больших отсортированных массивах, но требует предварительной сортировки.
Подготовка к собеседованию
  • На собеседованиях могут спросить:
    • Разницу между sort(), asort(), и ksort(). Ответ: sort() — по значению, сбрасывает ключи; asort() — по значению, сохраняет ключи; ksort() — по ключам.
    • Почему usort() медленнее sort(). Ответ: usort() вызывает пользовательскую функцию — медленнее встроенной sort().
    • Когда стоит использовать бинарный поиск вместо array_search(). Ответ: быстрее для отсортированных массивов (O(log n) vs O(n)).
    • Как реализовать стабильную сортировку в PHP. Ответ: оберни элементы с индексом:
    uasort($array, fn($a, $b) => $a['val'] <=> $b['val']);

Обработка ошибок: от @ к исключениям

Ранние версии PHP использовали оператор @ для подавления ошибок, но в современном коде предпочтительны исключения.

Ключевые моменты

  • Оператор @: Подавляет ошибки, но делает код менее прозрачным.
    $result = @file_get_contents('nonexistent.txt'); // Ошибка подавлена
    var_dump($result); // bool(false)
  • Исключения: Позволяют явно обрабатывать ошибки.
    try {
        $content = file_get_contents('nonexistent.txt');
    } catch (Exception $e) {
        echo "Ошибка: " . $e->getMessage();
    }
  • Throw как выражение (PHP 8): Позволяет использовать throw в тернарных операторах или коротких проверках.
    $value = 0;
    $result = $value ?: throw new InvalidArgumentException('Значение не может быть 0');
    var_dump($result); // Вызовет исключение

Подводные камни

  • Использование @ может скрыть критические ошибки, усложняя отладку.
  • В PHP 8 появились новые типы ошибок, такие как ValueError, что дает нам еще больше гибкости при отладке. Но многие все еще этим не пользуются, придумывая свои "велосипеды".
    // PHP 8
    $number = intdiv(10, 0); // ValueError: Division by zero

Совет: Избегайте @, используйте try-catch и логирование ошибок (например, с Monolog). На собеседованиях могут спросить, почему @ считается плохой практикой и как использовать throw в PHP 8.

Рекурсия и итерация

Рекурсия — процесс, при котором функция вызывает саму себя для решения задачи, разбивая её на более мелкие подзадачи. Это мощный инструмент, но он может быть ресурсоёмким и сложным для отладки. Итерация, использующая циклы, часто является альтернативой, которая может быть быстрее и потреблять меньше памяти. Этот раздел разбирает рекурсию, способы её замены и сравнение подходов.

Что такое рекурсия в PHP

Рекурсия в PHP работает, когда функция вызывает себя с новыми параметрами, пока не достигнет базового случая, который завершает выполнение. Каждый вызов создаёт новый кадр в стеке вызовов, что увеличивает потребление памяти.

Пример: Вычисление факториала (рекурсия):

function factorial($n) {
    if ($n <= 1) {
        return 1; // Базовый случай
    }
    return $n * factorial($n - 1); // Рекурсивный вызов
}

var_dump(factorial(5)); // int(120) — 5 * 4 * 3 * 2 * 1

Как это работает:

  • factorial(5) вызывает factorial(4), затем factorial(3), и так далее, пока не достигается factorial(1).
  • Стек вызовов растёт: [factorial(5), factorial(4), factorial(3), ...].
  • После достижения базового случая стек разворачивается, вычисляя результат.

Пример: Обход дерева (рекурсия):

function traverseTree($node) {
    if ($node === null) {
        return; // Базовый случай
    }
    echo $node['value'] . "\n";
    traverseTree($node['left']); // Рекурсия для левого поддерева
    traverseTree($node['right']); // Рекурсия для правого поддерева
}

$tree = [
    'value' => 1,
    'left' => ['value' => 2, 'left' => null, 'right' => null],
    'right' => ['value' => 3, 'left' => null, 'right' => null]
];
traverseTree($tree); // Вывод: 1, 2, 3

Переписывание рекурсии в итерацию

Рекурсию можно заменить итерацией, используя циклы (while, for) и, при необходимости, стек для имитации стека вызовов. Это снижает потребление памяти, так как не создаются новые кадры в стеке.

Факториал (итерация)

function factorialIterative($n) {
    $result = 1;
    for ($i = 1; $i <= $n; $i++) {
        $result *= $i;
    }
    return $result;
}

var_dump(factorialIterative(5)); // int(120)

Как это работает:

  • Вместо рекурсивных вызовов используется цикл, накапливающий результат.
  • Нет дополнительных кадров в стеке, только одна переменная $result.

Обход дерева (итерация с использованием стека)

function traverseTreeIterative($root) {
    if ($root === null) {
        return;
    }
    $stack = [$root];
    while (!empty($stack)) {
        $node = array_pop($stack);
        echo $node['value'] . "\n";
        // Добавляем правое поддерево первым, чтобы левое обработалось раньше
        if ($node['right'] !== null) {
            $stack[] = $node['right'];
        }
        if ($node['left'] !== null) {
            $stack[] = $node['left'];
        }
    }
}

traverseTreeIterative($tree); // Вывод: 1, 2, 3

Как это работает:

  • Стек ($stack) имитирует стек вызовов, сохраняя узлы для обработки.
  • Узлы извлекаются из стека, а их поддеревья добавляются в порядке, обеспечивающем тот же порядок обхода (префиксный).
  • Память используется только для стека, а не для кадров вызовов.

Анализ рекурсии и итерации

Производительность

  • Рекурсия:
    • Плюсы: Код часто короче и читаемее, особенно для задач, таких как обход дерева или рекурсивные алгоритмы (например, обход графа).
    • Минусы: Каждый вызов создаёт кадр в стеке, увеличивая потребление памяти (O(n) для глубины рекурсии). Глубокая рекурсия может вызвать переполнение стека (Fatal error: Maximum function nesting level).
    • Пример: Для factorial(10000) рекурсия может исчерпать стек на некоторых системах.
  • Итерация:
    • Плюсы: Не создаёт новых кадров, потребляет меньше памяти (O(1) для простых случаев, O(n) для стека при имитации рекурсии). Обычно быстрее из-за отсутствия накладных расходов на вызовы функций.
    • Минусы: Код может быть сложнее и менее интуитивным, особенно для древовидных структур.

Примерные результаты теста производительности (на 10,000 итераций, PHP 8.1, зависят от окружения):

  • factorial (recursive): ~0.08 сек
  • factorial (iterative): ~0.03 сек
  • traverseTree (recursive): ~0.06 сек
  • traverseTree (iterative): ~0.05 сек

Выводы:

  • Итеративный факториал значительно быстрее (~2.5 раза) из-за отсутствия накладных расходов на вызовы.
  • Итеративный обход дерева немного быстрее (~1.2 раза), но разница меньше из-за необходимости управления стеком.
  • Для больших входных данных рекурсия может привести к переполнению стека, тогда как итерация более устойчива.

Подводные камни

  • Рекурсия:
    • Переполнение стека: Глубокая рекурсия (например, factorial(100000)) может вызвать ошибку.
      factorial(100000); // Fatal error: Maximum function nesting level
    • Хвостовая рекурсия: PHP не оптимизирует хвостовую рекурсию, в отличие от некоторых языков (например, JavaScript с ES6). Это увеличивает потребление памяти.
      function factorialTail($n, $acc = 1) {
          if ($n <= 1) {
              return $acc;
          }
          return factorialTail($n - 1, $n * $acc);
      }
      // Всё равно создаёт стек вызовов
  • Итерация:
    • Сложность кода: Итеративные решения для сложных задач, таких как обход дерева, могут быть громоздкими.
    • Ошибки в управлении стеком: Неправильная работа со стеком (например, неверный порядок добавления узлов) может нарушить логику.
      // Ошибка: Неверный порядок добавления в стек
      $stack[] = $node['left'];
      $stack[] = $node['right']; // Поменяет порядок обхода

Когда использовать

  • Рекурсия:
    • Когда задача естественно рекурсивна (например, обход дерева, графов, рекурсивные алгоритмы, такие как быстрая сортировка).
    • Когда читаемость важнее производительности, а входные данные ограничены.
    • Пример: Обход файловой системы.
      function scanDirRecursive($dir) {
          if (!is_dir($dir)) {
              return;
          }
          echo $dir . "\n";
          foreach (scandir($dir) as $file) {
              if ($file !== '.' && $file !== '..') {
                  scanDirRecursive($dir . '/' . $file);
              }
          }
      }
  • Итерация:
    • Когда важна производительность или входные данные могут быть большими.
    • Когда нужно избежать переполнения стека.
    • Пример: Суммирование элементов массива.
      function arraySum($array) {
          $sum = 0;
          foreach ($array as $value) {
              $sum += $value;
          }
          return $sum;
      }

Подготовка к собеседованию

  • На собеседованиях часто спрашивают:
    • Что такое рекурсия и как она работает в PHP. Ответ: функция вызывает саму себя, пока не достигнет базового случая.
    • Как переписать рекурсивный код в итеративный (например, факториал, обход дерева). Ответ: с циклом и стеком/очередью:
    // Факториал
    function fact($n) {
      $res = 1;
      for ($i = 2; $i <= $n; $i++) $res *= $i;
      return $res;
    }
    • Почему рекурсия может привести к переполнению стека и как этого избежать. Ответ: может переполнить стек при глубокой вложенности. Избежать — использовать итерацию или tail recursion (не в PHP).
    • Разницу в производительности между рекурсией и итерацией. Ответ: итерация обычно быстрее и экономичнее по памяти — нет накладных расходов вызова функций.
  • Подготовьтесь объяснить, как стек вызовов влияет на память и как итерация с использованием стека может заменить рекурсию. Ответ: Каждый вызов функции в PHP (и в других языках) добавляется в стек вызовов (call stack). В этом стеке хранится: адрес возврата, локальные переменные, аргументы функции. Когда вызывается рекурсивная функция, каждый уровень рекурсии занимает новую ячейку в стеке. Если рекурсий слишком много — стек переполняется, и вы получаете “Fatal error: Maximum function nesting level”. Рекурсивные задачи можно преобразовать в итеративные, используя явный стек данных — обычный массив.
  • Практикуйтесь с задачами, такими как вычисление чисел Фибоначчи или обход дерева, в обоих подходах.

Классы и объекты

Объекты в PHP — основа ООП, но их поведение, особенно с магическими методами и сериализацией, полно неочевидных моментов. Этот раздел разбирает создание классов, наследование и типичные ошибки.

Ключевые моменты

  • Создание классов: Классы определяют свойства и методы, поддерживают наследование и интерфейсы.
    class User {
        public function __construct(public string $name) {}
    }
    $user = new User("Alice");
    var_dump($user->name); // string(5) "Alice"
  • Магические методы: Методы вроде __get, __set, __toString перехватывают действия.
    class Magic {
        private array $data = [];
        public function __set($name, $value) {
            $this->data[$name] = $value;
        }
        public function __get($name) {
            return $this->data[$name] ?? null;
        }
    }
    $obj = new Magic();
    $obj->key = 42;
    var_dump($obj->key); // int(42)

Типичные ошибки

  • Копирование объектов: Присваивание создаёт ссылку на тот же объект, а не копию.
    $user1 = new User("Alice");
    $user2 = $user1;
    $user2->name = "Bob";
    var_dump($user1->name); // string(3) "Bob" — $user1 изменён
    Решение: Используйте clone для создания копии.
    $user2 = clone $user1;
    $user2->name = "Bob";
    var_dump($user1->name); // string(5) "Alice"
  • Сериализация: Метод __sleep может ограничить сериализуемые свойства, но ошибки в нём ломают объект.
    class Test {
        public $data = 42;
        public function __sleep() {
            return ['data', 'wrong']; // Ошибка: свойство wrong не существует
        }
    }
    $obj = new Test();
    var_dump(serialize($obj)); // Warning: serialize(): "wrong" returned as member variable from __sleep()
    //but does not exist
    //string(31) "O:4:"Test":1:{s:4:"data";i:42;}"
  • Наследование: Неправильное использование parent может вызвать ошибки.
    class ParentClass {
       protected function test1() {
          return "Parent";
       }
       private function test2() {
          return "Parent";
       }
    }
    class ChildClass extends ParentClass {
       public function test1() {
          return parent::test1() . " Child";
       }
       public function test2() {
          return parent::test2() . " Child";
       }
       public function test3() {
          return parent::test3() . " Child";
       }
    }
    $child = new ChildClass();
    var_dump($child->test1()); // string(12) "Parent Child"
    var_dump($child->test2()); // Fatal error: Uncaught Error: Call to private method...
    var_dump($child->test3()); // Fatal error: Uncaught Error: Call to undefined method...

Производительность

Магические методы (__get, __set) замедляют доступ к свойствам из-за вызова функций.

Результаты теста производительности (1 млн итераций, PHP 8.1, ориентировочно):

  • Normal property: ~0.02 сек
  • Magic __get: ~0.06 сек

Подводные камни

  • Клонирование: Глубокое копирование требует реализации __clone.
    class Deep {
        public $data;
        public function __construct() {
            $this->data = new stdClass();
        }
        public function __clone() {
            $this->data = clone $this->data;
        }
    }
    $obj1 = new Deep();
    $obj2 = clone $obj1;
    $obj2->data->value = 42;
    var_dump($obj1->data->value ?? null); // NULL — копия независима
  • Сериализация и безопасность: Сериализованные объекты могут быть уязвимы (например, атака через __wakeup).
    class Vulnerable {
        public function __wakeup() {
            // Опасный код
        }
    }
  • Финализация: Метод __destruct не гарантирует немедленный вызов.
    class Temp {
        public function __destruct() {
            echo "Destroyed\n";
        }
    }
    $obj = new Temp();
    unset($obj); // Не всегда вызывает "Destroyed" сразу

Рекомендации

  • Используйте clone для копирования объектов, реализуйте __clone для глубокого копирования.
  • Избегайте магических методов в высоконагруженных системах из-за накладных расходов.
  • Проверяйте сериализуемые данные, чтобы избежать уязвимостей (например, атак через __wakeup).
  • Для сложных объектов используйте трейты вместо глубокого наследования, чтобы упростить код.
    trait Loggable {
        public function log($message) {
            echo "Log: $message\n";
        }
    }
    class User {
        use Loggable;
        public $name;
    }
    $user = new User();
    $user->log("User created"); // Log: User created

Подготовка к собеседованию

  • Часто спрашивают:
    • Разницу между присваиванием объекта и clone. Ответ: Присваивание— копирует ссылку, оба переменные указывают на один объект. Clone — создает новый объект с теми же свойствами (поверхностная копия).
    • Как работают магические методы (__get, __set, __toString). Ответ: __get($name) — вызывается при доступе к несуществующему/приватному свойству. __set($name, $value) — вызывается при присвоении несуществующему/приватному свойству. __toString() — вызывается при попытке преобразовать объект в строку (echo $obj).
    • Как избежать уязвимостей при сериализации. Ответ: Не доверяй unserialize() с внешними данными — может запустить __wakeup() или __destruct() злоумышленника. Используй json_encode() / json_decode() для безопасного хранения. Или перед unserialize() — задавай список допустимых классов.
    • Почему __destruct не всегда вызывается сразу. Ответ: вызывается при удалении последней ссылки на объект. Если объект всё ещё где-то в памяти — деструктор ждёт. При циклических ссылках может вообще не вызваться сразу (до сборки мусора).
  • Подготовьтесь объяснить, как __clone влияет на глубокое копирование и как трейты упрощают ООП.
  • Практикуйтесь с задачами, например, реализация __toString для вывода объекта или обработка __wakeup для безопасной десериализации.

PSR — PHP Standards Recommendations

PSR (PHP Standards Recommendations) — это набор стандартов, разработанный группой PHP-FIG (Framework Interop Group), чтобы унифицировать подходы к разработке, сделать код более читаемым, предсказуемым и совместимым между фреймворками и библиотеками.

Ключевые моменты

  • PSR-1: Базовый стиль кода (StudlyCaps, camelCase).
  • PSR-12: Расширенный стиль (отступы, strict_types).
  • PSR-3: Логирование.
  • PSR-4: Автозагрузка.
    "autoload": {
        "psr-4": { "App\\": "src/" }
    }
  • PSR-7: HTTP-сообщения.
    use Psr\Http\Message\ResponseInterface;
    use Laminas\Diactoros\Response;
    function createResponse(): ResponseInterface {
        $response = new Response();
        $response->getBody()->write('Hello');
        return $response;
    }
  • PSR-11: DI-контейнер.
  • PSR-13: HATEOAS-ссылки.
  • PSR-15: Middleware.
  • PSR-17: HTTP-фабрики.
  • PSR-18: HTTP-клиент.

Подводные камни

  • Совместимость: Не все библиотеки строго следуют PSR.
  • Сложность: PSR-7 требует больше кода.

Рекомендации

  • Используйте PSR-7 в API для переносимости.
  • Настраивайте PSR-4 через Composer.

Подготовка к собеседованию

  • Вопросы:
    • Что определяет PSR-4? Ответ: стандарт автозагрузки классов: пространство имён ⇔ структура директорий.
    • Как PSR-7 используется в фреймворках? Ответ: интерфейсы для HTTP-запросов/ответов (Request/Response). Во фреймворках (Laravel, Symfony, Slim) используется для совместимости middleware и роутеров.
  • Подготовка: Реализуйте middleware с PSR-15.

SPL и автозагрузка

spl_autoload_register()

Позволяет задать свою функцию автозагрузки классов (альтернатива Composer autoload):

spl_autoload_register(function ($class) {
    include 'src/' . $class . '.php';
});
  • Используется в собственных фреймворках, старых проектах или системах без Composer.
  • Поддерживает стек автозагрузчиков — можно зарегистрировать несколько функций.

Composer — Менеджер зависимостей для PHP

Composer — это стандартный инструмент для управления зависимостями и автозагрузкой в PHP. Он позволяет подключать сторонние библиотеки, управлять версиями и конфигурацией проекта.

Основные команды

Установка пакета

composer require vendor/package

Добавляет зависимость и сразу обновляет composer.json и composer.lock.

🧹 Удаление пакета

composer remove vendor/package

Удаляет пакет и очищает зависимости.

Обновление всех зависимостей

composer update

Обновляет все зависимости до последних допустимых версий согласно composer.json.

Обновление одного пакета

composer update vendor/package

Структура файлов

composer.json

Файл конфигурации проекта:

  • Список зависимостей и их версий.
  • Схема автозагрузки (PSR-4).
  • Метаданные пакета (название, авторы, лицензия и т.д.)

Пример:

{
  "name": "yourname/project",
  "require": {
    "monolog/monolog": "^2.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}

composer.lock

  • Фиксирует точные версии всех установленных зависимостей и подзависимостей.
  • Используется для воспроизводимости (CI/CD, деплой).
  • Нужен в проекте! Его нужно коммитить, если проект — приложение (а не библиотека).

Когда не коммитить composer.lock?

  • Когда вы разрабатываете библиотеку, и не хотите фиксировать зависимости для других.

Создание своего Composer-пакета

  1. Создай новый репозиторий с composer.json:
composer init
  1. Пример composer.json:
{
  "name": "vendorname/mypackage",
  "description": "Custom helper package",
  "type": "library",
  "autoload": {
    "psr-4": {
      "MyPackage\\": "src/"
    }
  },
  "require": {}
}
  1. Структура проекта:
mypackage/
├── src/
│   └── Helper.php
├── composer.json
  1. Опубликуй на GitHub и добавь версию с git-тегом:
git tag v1.0.0
git push origin v1.0.0
  1. 📢 Зарегистрируй на Packagist

Локальное подключение своего пакета (без Packagist)

В проекте, где хочешь использовать пакет:

"repositories": [
  {
    "type": "vcs",
    "url": "https://github.com/vendorname/mypackage"
  }
],
"require": {
  "vendorname/mypackage": "dev-main"
}

Подводные камни

  • Конфликты версий: Разные пакеты могут требовать несовместимые версии зависимостей. Проверяйте конфликты перед обновлением.

    composer why-not vendor/package 1.2.3
  • Память: Большие проекты могут исчерпать лимит памяти.

    ini_set('memory_limit', '512M');
  • Игнорирование composer.lock: Без него CI/CD может сломаться из-за неожиданных версий.

  • Локальные пакеты: dev-main может быть нестабильным, используйте теги (например, v1.0.0).

Рекомендации

  • Проверяйте composer.json:
    composer validate
  • Ищите устаревшие зависимости:
    composer outdated
  • Оптимизируйте автозагрузку:
    composer dump-autoload --optimize
  • Используйте ^ для версий (^2.0 — любые 2.x) и ~ для патчей (~1.21.2.*).

Мой совет: всегда коммитьте composer.lock в приложениях! Иначе ваш прод превратится в лотерею.

Подготовка к собеседованию

  • Вопросы:
    • Зачем нужен composer.lock? Когда его не коммитить? Ответ: фиксирует версии зависимостей. Не коммитят — только в библиотеках, чтобы не навязывать версии.
    • Как создать и опубликовать свой пакет? Ответ: composer init, указать autoload, написать код → опубликовать на packagist.org.
    • Как подключить локальный пакет без Packagist? Ответ:
    "repositories": [
       { "type": "path", "url": "../my-lib" }
    ]
    И затем composer require my/vendor:*.
  • Подготовка: Опубликуйте тестовый пакет на GitHub и настройте его в другом проекте. Объясните разницу между ^ и ~.

Линтеры: чистый код для ленивых

Что такое линтеры?

Линтер (от английского lint) — это инструмент, который статически анализирует код, вылавливая ошибки, несоответствия стандартам и потенциальные баги, не запуская программу.

Зачем они нужны?

  • Чистота кода: Линтеры находят баги вроде if($x=1) вместо if($x==1).
  • Единый стиль: Команда не будет драться из-за пробелов vs табов.
  • Меньше багов: Вылавливают подозрительные места до продакшена.
  • Учёба: Новички учатся писать по стандартам (например, PSR-12).

Популярные линтеры для PHP

  1. PHP_CodeSniffer (phpcs):
  • Что делает: Проверяет код на соответствие стандартам (PSR-12, PSR-2).
  • Установка: composer require --dev squizlabs/php_codesniffer.
  • Пример:
    ./vendor/bin/phpcs --standard=PSR12 index.php
    Вывод:
    FILE: index.php
    ----------------------------------------------------------------------
    FOUND 1 ERROR
    2 | ERROR | Missing semicolon
    ----------------------------------------------------------------------
    
  • Плюсы: Гибкий, поддерживает кастомные правила.
  • Минусы: Медленный на больших проектах.
  1. PHPStan:
  • Что делает: Ищет баги и логические ошибки (например, вызов несуществующего метода).
  • Установка: composer require --dev phpstan/phpstan.
  • Пример:
    ./vendor/bin/phpstan analyse index.php --level=5
    Вывод:
    [ERROR] Method App\Foo::bar() does not exist.
    
  • Плюсы: Ловит сложные баги, уровни строгости.
  • Минусы: Требует настройки для старого кода.
  1. Psalm:
  • Что делает: Как PHPStan, но с фокусом на типизацию и аннотации.
  • Установка: composer require --dev vimeo/psalm.
  • Пример:
    ./vendor/bin/psalm index.php
  • Плюсы: Отличен для проектов с типами.
  • Минусы: Сложнее для новичков.

Интеграция

  • PhpStorm:

    1. Убедись, что линтеры установлены:
    • PHP_CodeSniffer: composer require --dev squizlabs/php_codesniffer.
    • PHPStan: composer require --dev phpstan/phpstan.
    1. Настрой PHP_CodeSniffer:
    • File > Settings > Languages & Frameworks > PHP > Quality Tools > PHP_CodeSniffer.
    • Путь: vendor/bin/phpcs (в папке проекта).
    • Включи: Enable, стандарт — PSR12.
    • В Editor > Inspections включи PHP_CodeSniffer validation (PSR12).
    • Результат: Подсветка багов:
      if($x=1)echo "Bug!"; // Ошибка: "Use ==, add spaces, semicolon!"
      Наведи, жми Alt+EnterFix.
    1. Настрой PHPStan:
    • Settings > Languages & Frameworks > PHP > Quality Tools > PHPStan.
    • Путь: vendor/bin/phpstan, уровень — 5.
    • В Inspections включи PHPStan validation.
    • Результат: Ловит баги:
      class Foo { function bar() {} }
      $foo = new Foo();
      $foo->baz(); // Ошибка: "Method baz() not found!"
    1. Работай как профи:
    • Ошибки подсвечиваются в реальном времени.
    • Проверяй проект: Code > Inspect Code.
    • Автофикс: Code > Reformat Code.
  • CI/CD: Добавь линтер в GitHub Actions:

    name: Lint
    on: [push]
    jobs:
      lint:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - uses: shivammathur/setup-php@v2
            with:
              php-version: '8.2'
          - run: composer install
          - run: ./vendor/bin/phpcs --standard=PSR12 .
  • Работа в комманде: Договоритесь о стандарте (например PSR-12).

Конфигурация линтеров

Настрой линтеры под проект через конфиги, чтобы не писать --standard=PSR12 каждый раз и адаптировать правила.

  1. PHP_CodeSniffer:
  • Создай phpcs.xml в корне проекта:
    <?xml version="1.0"?>
    <ruleset name="MyProject">
        <description>PSR-12 with tweaks</description>
        <file>.</file>
        <exclude-pattern>vendor/</exclude-pattern>
        <rule ref="PSR12"/>
        <!-- Отключи проверку длины строки -->
        <rule ref="Generic.Files.LineLength">
            <exclude name="Generic.Files.LineLength.TooLong"/>
        </rule>
    </ruleset>
  • Используй: ./vendor/bin/phpcs (автоматически читает phpcs.xml).
  1. PHPStan:
  • Создай phpstan.neon:
    parameters:
        level: 5
        paths:
            - src
            - tests
        excludePaths:
            - vendor
  • Используй: ./vendor/bin/phpstan analyse.
  1. Psalm:
  • Создай psalm.xml:
    <?xml version="1.0"?>
    <psalm
        errorLevel="3"
        resolveFromConfigFile="true">
        <projectFiles>
            <directory name="src"/>
            <ignoreFiles>
                <directory name="vendor"/>
            </ignoreFiles>
        </projectFiles>
    </psalm>
  • Используй: ./vendor/bin/psalm.

Игнорирование проверок

Иногда линтеры мешают (например, старый код или хак). Для таких случаев можно написать комментарии для игнорирования куска кода линтером.

  1. PHP_CodeSniffer:
  • Строка: Добавь // phpcs:ignore:
    if($x=1) echo "Hack!"; // phpcs:ignore
  • Метод: Используй // phpcs:disable и // phpcs:enable:
    // phpcs:disable
    function messyCode() {
        if($x=1)echo "Ugly!";
    }
    // phpcs:enable
  • Файл: В начале файла:
    <?php // phpcs:ignoreFile
    $x=1; if($x=2)echo "Chaos!";
  1. PHPStan:
  • Строка: Добавь // @phpstan-ignore-next-line:
    $foo->baz(); // @phpstan-ignore-next-line
  • Метод: Используй блок игнора:
    /** @phpstan-ignore */
    function risky() {
        $foo->baz();
    }
  • Файл: В phpstan.neon исключи файл:
    parameters:
        excludePaths:
            - src/legacy.php
  1. Psalm:
  • Строка: // psalm-suppress:
    $foo->baz(); // psalm-suppress UndefinedMethod
  • Метод: Аннотация:
    /** @psalm-suppress UndefinedMethod */
    function risky() {
        $foo->baz();
    }
  • Файл: В psalm.xml:
    <ignoreFiles>
        <file name="src/legacy.php"/>
    </ignoreFiles>

Предупреждение: Игнор линтера (как и // TODO) используй максимально редко, и только там, где это действительно нужно. Хороший тон — указывать в комментарии номер задачи в которой планируется этот кусок кода исправить либо почему конкретно стоит игнор.

Вопросы на собеседованиях

Линтеры — частая тема на собесах, особенно для джунов и мидлов. Так что если ты их еще не используешь, самое время начать.

  1. Что такое линтеры и зачем они нужны в PHP?
  • Ответ: Линтеры — инструменты статического анализа, которые находят ошибки стиля (например, PSR-12) и баги (например, вызов несуществующего метода) без запуска кода. Они обеспечивают чистоту кода, единый стиль в команде и снижают баги. Примеры: PHP_CodeSniffer для стиля, PHPStan для логики.
  • Подвох: Спросят про стандарты (PSR-12) или разницу линтеров и тестирования.
  1. Какой линтер вы использовали и как его настраивали?
  • Ответ: Использовал PHP_CodeSniffer с PSR-12 через phpcs.xml для проверки стиля и PHPStan с phpstan.neon (уровень 5) для багов. Настраивал в PhpStorm: указал пути к vendor/bin/phpcs и vendor/bin/phpstan, включил проверки в Inspections.
  • Подvoх: Могут попросить пример конфига или интеграции в CI/CD.
  1. Как игнорировать проверку линтера для старого кода?
  • Ответ: Для PHP_CodeSniffer — // phpcs:ignore для строки или phpcs:ignoreFile для файла. Для PHPStan — // @phpstan-ignore-next-line или исключение файла в phpstan.neon. Для Psalm — // psalm-suppress или исключение в psalm.xml. Но игнор — последнее средство, лучше фиксить код.
  • Подвох: Спросят, как балансировать игноры и рефакторинг.