Senior: питання на співбесіді з теми «ООП»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Успадкування - найсильніший зв'язок між класами: нащадок залежить від усіх деталей предка, зокрема захищених. Будь-яка зміна в базовому класі може тихо зламати десяток нащадків - це називають проблемою крихкого базового класу.
Типові симптоми:
- Ієрархія росте вглиб:
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 (гроші, діапазони дат, адреси) можна спокійно передавати куди завгодно - ніхто не змінить їх «під ногами» в іншому місці коду.
clone $obj створює новий об'єкт з тими самими значеннями властивостей. Але копія поверхнева: якщо властивість містить інший об'єкт, копіюється лише посилання на нього - обидва об'єкти ділять той самий вкладений об'єкт.
final class Order
{
public function __construct(public DateTime $createdAt, public array $items) {}
}
$a = new Order(new DateTime('2026-01-01'), ['book']);
$b = clone $a;
$b->items[] = 'pen'; // масив скопійовано: $a->items не змінився
$b->createdAt->modify('+1 day'); // об'єкт спільний: $a->createdAt теж змінився!
Масиви копіюються (за значенням), об'єкти всередині - ні.
__clone() викликається на копії після клонування - там роблять глибоку копію вкладених об'єктів:
public function __clone(): void
{
$this->createdAt = clone $this->createdAt;
}
Де це стає проблемою:
- «Незмінні» об'єкти з змінюваними частинами. Wither, що робить
clone $thisі змінює одне поле, ділить усі вкладені змінювані об'єкти з оригіналом. - Ресурси й з'єднання. Клон об'єкта з відкритим файлом чи з'єднанням з базою ділить той самий ресурс; закриття в одному ламає інший.
- Ідентичність: клон Eloquent-моделі зберігає той самий первинний ключ. Для копії запису в базі є
replicate().
Як уникнути проблем: робити вкладені значення незмінними (DateTimeImmutable, readonly value objects) - тоді поверхнева копія безпечна. З PHP 8.3 readonly-властивості можна переприсвоїти в __clone(), а PHP 8.5 додав clone($obj, ['prop' => $value]) для «зміненої копії» без ручного __clone.
Property hooks (PHP 8.4) дозволяють прив'язати логіку до читання й запису властивості - без геттерів і сеттерів і без магічних __get/__set.
final class User
{
public string $email {
set (string $value) {
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Некоректний email');
}
$this->email = mb_strtolower($value);
}
}
public string $displayName {
get => trim("{$this->firstName} {$this->lastName}"); // віртуальна властивість
}
public function __construct(public string $firstName, public string $lastName, string $email)
{
$this->email = $email;
}
}
$user->email = 'Olia@Example.com'; // спрацює set-хук
echo $user->displayName; // спрацює get-хук
Властивість, що має лише get-хук і не звертається до власного значення, - віртуальна: місця для неї не виділяється.
Асиметрична видимість - різні модифікатори для читання й запису:
final class Order
{
public private(set) string $status = 'new'; // читати - всім, змінювати - лише класу
public function ship(): void
{
$this->status = 'shipped';
}
}
Скорочення: private(set) string $status означає public private(set).
Що це змінює:
- Публічна властивість більше не означає «будь-хто може записати що завгодно» - валідацію й нормалізацію можна додати пізніше, не змінюючи API на методи.
- Відпадає шаблонний код геттерів для «читається всім, змінюється лише всередині».
- Хуки можна оголошувати в інтерфейсах:
public string $name { get; }.
Обмеження: хуки й readonly не поєднуються, а посилання на властивість з set-хуком (&$obj->prop) заборонене.