PHP: Senior
20 питань · ~22 хв · Версія v3.0
Увійдіть, щоб продовжити
Глибокі знання мови й рантайму: пам'ять і GC, генератори й Fiber, атрибути, типізація, продуктивність.
- За спробу
- 20
- У пулі
- 100
- Проходжень
- 1
- Середній бал
- 50%
- Пройшли на 70%+
- 0%
Питання для підготовки
33 питанняPHP використовує підрахунок посилань + збирач циклічних посилань. У звичайному веб-запиті пам'ять звільняється наприкінці запиту, тож витоки малопомітні. Але у довготривалих процесах (черги, Octane) пам'ять накопичується.
Як уникати:
- Не зберігати стан у статичних властивостях/синглтонах між завданнями.
unset()великих структур, скидати накопичувачі (логи запитівDB::flushQueryLog()).- Обробляти дані порціями (
chunk,lazy), не тримати все в пам'яті. - Перезапускати воркери за лімітом:
queue:work --max-jobs=1000 --max-time=3600або при досягненні--memory.
Octane має gc_collect_cycles()-хуки; Horizon автоматично перезапускає воркери, що «розпухли».
Enum у коді змінюється легко, а от рядки, які вже лежать у базі, - ні. Саме тут зʼявляються помилки після деплою.
Найнебезпечніше - перейменувати кейс:
// було
case Middle = 'middle';
// стало
case Mid = 'mid';
Код збереться, а всі наявні рядки зі значенням middle перестануть кастуватися: ValueError: "middle" is not a valid backing value. Впаде не міграція, а звичайна сторінка.
Правильний порядок для перейменування:
- Додати новий кейс, лишивши старий.
- Міграцією перевести дані:
Vacancy::where('level', 'middle')->update(['level' => 'mid']). - Наступним релізом прибрати старий кейс.
Додати новий кейс - безпечно, якщо колонка varchar. Але коли в базі використано нативний тип enum, потрібна ще й міграція самої колонки, а Schema::table()->change() для нативних enum працює не в усіх драйверах - подекуди доводиться писати DB::statement().
Тому колонку під enum майже завжди роблять string: перелік живе в PHP, база зберігає рядок, і зміни не потребують ALTER на великій таблиці.
Захист від падіння на невідомому значенні:
// null замість винятку, коли в базі щось несподіване
$level = VacancyLevel::tryFrom($vacancy->getRawOriginal('level'));
І ще одне: якщо enum використовується у валідації через Rule::enum(), видалений кейс одразу зробить старі збережені записи невалідними при редагуванні - про це згадують уже після скарг користувачів.
Успадкування - найсильніший зв'язок між класами: нащадок залежить від усіх деталей предка, зокрема захищених. Будь-яка зміна в базовому класі може тихо зламати десяток нащадків - це називають проблемою крихкого базового класу.
Типові симптоми:
- Ієрархія росте вглиб:
Report→PdfReport→ColoredPdfReport→ ... Нова комбінація ознак - новий клас. - Нащадок перевизначає метод, щоб «вимкнути» поведінку предка, яка йому не потрібна.
- Базовий клас обростає
protected-хелперами, бо так зручніше нащадкам.
Композиція - об'єкт отримує інші об'єкти і делегує їм роботу:
final class ReportExporter
{
public function __construct(
private Formatter $formatter, // PDF, CSV, HTML
private Storage $storage, // диск, S3
) {}
public function export(Report $report): string
{
return $this->storage->put($this->formatter->format($report));
}
}
Тепер формат і сховище комбінуються вільно, кожне тестується окремо, і змінити одне можна, не чіпаючи іншого.
Успадкування доречне, коли є справжнє «є різновидом» і спільний контракт стабільний: власні винятки від RuntimeException, моделі від Model. Корисна звичка - робити класи final за замовчуванням: тоді успадкування стає свідомим рішенням, а не випадковістю.
readonly-властивість (PHP 8.1) можна присвоїти лише один раз і лише всередині класу - зазвичай у конструкторі. Далі будь-яка спроба змінити її кидає Error. readonly class (PHP 8.2) робить такими всі властивості класу.
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
}
Що варто знати:
readonlyзахищає саму властивість, а не вміст об'єкта в ній: якщо там лежить змінюваний об'єкт, його поля змінювати можна.- Властивість мусить мати тип, і значення за замовчуванням у неї бути не може.
Змінені копії («withers»). Незмінний об'єкт не редагують, а створюють новий з іншим значенням:
public function withAmount(int $amount): static
{
return new static($amount, $this->currency);
}
Через clone раніше так не виходило: копія мала ту саму вже ініціалізовану властивість. З PHP 8.3 readonly-властивості можна переприсвоїти в __clone(), а PHP 8.5 додав clone з переліком нових значень - clone($this, ['amount' => $amount]), без ручного конструктора.
Навіщо це все: незмінні value objects (гроші, діапазони дат, адреси) можна спокійно передавати куди завгодно - ніхто не змінить їх «під ногами» в іншому місці коду.
Масиви в PHP передаються за значенням, але копіюються ліниво - за принципом copy-on-write. Присвоєння чи передача в функцію не копіює дані, а лише збільшує лічильник посилань. Справжня копія з'являється в момент, коли одну з «копій» змінюють.
$a = range(1, 1_000_000);
$b = $a; // пам'ять не виросла: обидві змінні дивляться на ті самі дані
$b[] = 1; // тепер копія - і ще ~кілька десятків МБ
Що з цього випливає:
- Передавати великий масив у функцію дешево, поки функція його не змінює. Передача за посиланням (
&$items) «для швидкодії» зазвичай нічого не дає. - Посилання можуть, навпаки, спричинити копіювання: змішування посилань і звичайних змінних на ті самі дані змушує PHP розділяти масив раніше.
- Класична пастка -
foreach ($items as &$item)безunset($item)після циклу: змінна лишається посиланням на останній елемент, і наступнийforeachза значенням його перезапише.
Об'єкти поводяться інакше: змінна зберігає ідентифікатор об'єкта, тож передача в функцію дає доступ до того самого об'єкта. Для копії потрібен clone.
Спершу почитати
Прочитати - ще не значить знати
20 питань, по одному на екран, ~22 хв. Після завершення - розбір кожної помилки з посиланням на питання.