Эта глава — компактный обзор основ 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) — значение равно nullempty()возвращает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().
- Поведение: Оба проверяют, является ли значение строго
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(), а также их влияние на производительность.
В PHP по умолчанию используется автоматическое приведение типов (type juggling), что может привести к неожиданным результатам и ошибкам, которые сложно отследить. Директива strict_types помогает сделать код предсказуемым, но действует только на вызовы функций и методов (проверка аргументов и возвращаемых типов).
<?php
declare(strict_types=1);🔺 Это объявление должно быть первой строкой файла, до любого вывода или кода. Оно влияет только на текущий файл и не распространяется на включённые файлы (например, через require).
function sum(int $a, int $b): int {
return $a + $b;
}
echo sum(2, 3.5); // 5 — float приводится к intdeclare(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
- Тихое приведение типов (
float→int,string→intи т.д.). - Логические ошибки, которые трудно отследить.
- Непредсказуемое поведение, зависящее от входных данных.
- Защищает от скрытых ошибок при передаче аргументов или возврате значений.
- Делает типы прозрачными и предсказуемыми.
- Упрощает написание юнит-тестов.
- Обеспечивает совместимость с современными стандартами и инструментами статического анализа (
PHPStan,Psalm).
- Всегда включайте
strict_types=1в каждом PHP-файле, особенно в библиотеках или API. - Явно указывайте типы параметров и возвращаемых значений (
int,string,array,bool, и т.д.). - Используйте в связке с авто-тестами и статическим анализом для максимальной надёжности.
Ссылки и переменные переменных в PHP — мощные, но неочевидные инструменты, которые могут упростить код или усложнить отладку. В PHP 8 ссылки (&) позволяют передавать переменные, не создавая их копий в памяти (ссылаться одну область памяти), а переменные переменных ($$var) позволяют динамически обращаться к переменным. Этот раздел разбирает их применение, подводные камни.
Ссылки позволяют нескольким переменным указывать на одну и ту же область памяти. Оператор & используется для создания ссылок при передаче в функции, в циклах или при присваивании.
- Передача в функции: Позволяет функции изменять исходную переменную.
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.
Результаты (PHP 8.1, ориентировочно):
$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";
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медленнее, чем через массив.Результаты (PHP 8.1, ориентировочно):$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";
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'); // Тест <b>
- Обрезка строк:
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(): Создаёт массив, используя один массив как ключи, а другой как значения.Пример с БД: Создание словаря ID → имя из результата запроса.$keys = ["id1", "id2"]; $values = ["Alice", "Bob"]; var_dump(array_combine($keys, $values)); // ["id1" => "Alice", "id2" => "Bob"]
Подводный камень: Оба массива должны иметь одинаковую длину, иначе ошибка.$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 работает, когда функция вызывает себя с новыми параметрами, пока не достигнет базового случая, который завершает выполнение. Каждый вызов создаёт новый кадр в стеке вызовов, что увеличивает потребление памяти.
Пример: Вычисление факториала (рекурсия):
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) — это набор стандартов, разработанный группой 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.
Позволяет задать свою функцию автозагрузки классов (альтернатива Composer autoload):
spl_autoload_register(function ($class) {
include 'src/' . $class . '.php';
});- Используется в собственных фреймворках, старых проектах или системах без Composer.
- Поддерживает стек автозагрузчиков — можно зарегистрировать несколько функций.
Composer — это стандартный инструмент для управления зависимостями и автозагрузкой в PHP. Он позволяет подключать сторонние библиотеки, управлять версиями и конфигурацией проекта.
composer require vendor/packageДобавляет зависимость и сразу обновляет composer.json и composer.lock.
composer remove vendor/packageУдаляет пакет и очищает зависимости.
composer updateОбновляет все зависимости до последних допустимых версий согласно composer.json.
composer update vendor/packageФайл конфигурации проекта:
- Список зависимостей и их версий.
- Схема автозагрузки (PSR-4).
- Метаданные пакета (название, авторы, лицензия и т.д.)
Пример:
{
"name": "yourname/project",
"require": {
"monolog/monolog": "^2.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}- Фиксирует точные версии всех установленных зависимостей и подзависимостей.
- Используется для воспроизводимости (
CI/CD, деплой). - Нужен в проекте! Его нужно коммитить, если проект — приложение (а не библиотека).
Когда не коммитить composer.lock?
- Когда вы разрабатываете библиотеку, и не хотите фиксировать зависимости для других.
- Создай новый репозиторий с
composer.json:
composer init- Пример
composer.json:
{
"name": "vendorname/mypackage",
"description": "Custom helper package",
"type": "library",
"autoload": {
"psr-4": {
"MyPackage\\": "src/"
}
},
"require": {}
}- Структура проекта:
mypackage/
├── src/
│ └── Helper.php
├── composer.json
- Опубликуй на GitHub и добавь версию с git-тегом:
git tag v1.0.0
git push origin v1.0.0- 📢 Зарегистрируй на 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.2—1.2.*).
Мой совет: всегда коммитьте
composer.lockв приложениях! Иначе ваш прод превратится в лотерею.
- Вопросы:
- Зачем нужен
composer.lock? Когда его не коммитить? Ответ: фиксирует версии зависимостей. Не коммитят — только в библиотеках, чтобы не навязывать версии. - Как создать и опубликовать свой пакет? Ответ: composer init, указать autoload, написать код → опубликовать на packagist.org.
- Как подключить локальный пакет без Packagist? Ответ:
И затем composer require my/vendor:*."repositories": [ { "type": "path", "url": "../my-lib" } ]
- Зачем нужен
- Подготовка: Опубликуйте тестовый пакет на GitHub и настройте его в другом проекте. Объясните разницу между
^и~.
Линтер (от английского lint) — это инструмент, который статически анализирует код, вылавливая ошибки, несоответствия стандартам и потенциальные баги, не запуская программу.
Зачем они нужны?
- Чистота кода: Линтеры находят баги вроде
if($x=1)вместоif($x==1). - Единый стиль: Команда не будет драться из-за пробелов vs табов.
- Меньше багов: Вылавливают подозрительные места до продакшена.
- Учёба: Новички учатся писать по стандартам (например, PSR-12).
- 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 ---------------------------------------------------------------------- - Плюсы: Гибкий, поддерживает кастомные правила.
- Минусы: Медленный на больших проектах.
- PHPStan:
- Что делает: Ищет баги и логические ошибки (например, вызов несуществующего метода).
- Установка:
composer require --dev phpstan/phpstan. - Пример:
Вывод:
./vendor/bin/phpstan analyse index.php --level=5
[ERROR] Method App\Foo::bar() does not exist. - Плюсы: Ловит сложные баги, уровни строгости.
- Минусы: Требует настройки для старого кода.
- Psalm:
- Что делает: Как PHPStan, но с фокусом на типизацию и аннотации.
- Установка:
composer require --dev vimeo/psalm. - Пример:
./vendor/bin/psalm index.php
- Плюсы: Отличен для проектов с типами.
- Минусы: Сложнее для новичков.
-
PhpStorm:
- Убедись, что линтеры установлены:
- PHP_CodeSniffer:
composer require --dev squizlabs/php_codesniffer. - PHPStan:
composer require --dev phpstan/phpstan.
- Настрой 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+Enter→Fix.
- Настрой 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!"
- Работай как профи:
- Ошибки подсвечиваются в реальном времени.
- Проверяй проект:
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 каждый раз и адаптировать правила.
- 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).
- PHPStan:
- Создай
phpstan.neon:parameters: level: 5 paths: - src - tests excludePaths: - vendor
- Используй:
./vendor/bin/phpstan analyse.
- 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.
Иногда линтеры мешают (например, старый код или хак). Для таких случаев можно написать комментарии для игнорирования куска кода линтером.
- 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!";
- PHPStan:
- Строка: Добавь
// @phpstan-ignore-next-line:$foo->baz(); // @phpstan-ignore-next-line
- Метод: Используй блок игнора:
/** @phpstan-ignore */ function risky() { $foo->baz(); }
- Файл: В
phpstan.neonисключи файл:parameters: excludePaths: - src/legacy.php
- 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) используй максимально редко, и только там, где это действительно нужно. Хороший тон — указывать в комментарии номер задачи в которой планируется этот кусок кода исправить либо почему конкретно стоит игнор.
Линтеры — частая тема на собесах, особенно для джунов и мидлов. Так что если ты их еще не используешь, самое время начать.
- Что такое линтеры и зачем они нужны в PHP?
- Ответ: Линтеры — инструменты статического анализа, которые находят ошибки стиля (например, PSR-12) и баги (например, вызов несуществующего метода) без запуска кода. Они обеспечивают чистоту кода, единый стиль в команде и снижают баги. Примеры: PHP_CodeSniffer для стиля, PHPStan для логики.
- Подвох: Спросят про стандарты (PSR-12) или разницу линтеров и тестирования.
- Какой линтер вы использовали и как его настраивали?
- Ответ: Использовал PHP_CodeSniffer с PSR-12 через
phpcs.xmlдля проверки стиля и PHPStan сphpstan.neon(уровень 5) для багов. Настраивал в PhpStorm: указал пути кvendor/bin/phpcsиvendor/bin/phpstan, включил проверки в Inspections. - Подvoх: Могут попросить пример конфига или интеграции в CI/CD.
- Как игнорировать проверку линтера для старого кода?
- Ответ: Для PHP_CodeSniffer —
// phpcs:ignoreдля строки илиphpcs:ignoreFileдля файла. Для PHPStan —// @phpstan-ignore-next-lineили исключение файла вphpstan.neon. Для Psalm —// psalm-suppressили исключение вpsalm.xml. Но игнор — последнее средство, лучше фиксить код. - Подвох: Спросят, как балансировать игноры и рефакторинг.