Питання на співбесіді: ООП
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
12 питань
Інтерфейс - це лише контракт: перелік публічних методів, які клас зобов'язується мати. Реалізації в ньому немає (крім констант), і клас може реалізувати скільки завгодно інтерфейсів.
Абстрактний клас - це недобудований клас: у ньому можуть бути властивості, готові методи й абстрактні методи, які мають дописати нащадки. Успадкувати можна лише один клас.
interface PaymentGateway
{
public function charge(Money $amount): Receipt;
}
abstract class HttpGateway implements PaymentGateway
{
public function __construct(protected HttpClient $http) {}
protected function post(string $path, array $data): array
{
return $this->http->post($this->baseUrl() . $path, $data);
}
abstract protected function baseUrl(): string;
}
Коли що:
- Інтерфейс - коли потрібна взаємозамінність: код залежить від
PaymentGateway, а конкретний шлюз підставляється ззовні. - Абстрактний клас - коли кілька реалізацій ділять спільний код і стан.
Часто вони йдуть разом, як у прикладі: залежать від інтерфейсу, а абстрактний клас лише прибирає повтор між реалізаціями.
Це модифікатори видимості - хто може звертатися до властивості чи методу:
public- будь-хто, зокрема код поза класом.protected- сам клас і його нащадки.private- лише сам клас, де член оголошено. Нащадок його не бачить.
class Account
{
public function __construct(private int $balance = 0) {}
public function deposit(int $amount): void
{
$this->guard($amount);
$this->balance += $amount;
}
protected function guard(int $amount): void
{
if ($amount <= 0) {
throw new InvalidArgumentException('Сума має бути додатною');
}
}
}
Тут баланс не можна змінити напряму ззовні - лише через deposit(), який перевіряє суму. У цьому й сенс інкапсуляції: клас сам стежить за тим, щоб його стан лишався коректним.
Правило за замовчуванням: усе private, доки не з'явиться причина відкрити більше. Відкриту властивість потім важко закрити - на неї вже покладається чужий код.
У PHP 8.4 з'явилася асиметрична видимість: public private(set) int $balance - читати можна звідусіль, а змінювати лише всередині класу.
Просування властивостей (PHP 8.0) дозволяє оголосити властивість і присвоїти їй значення прямо в параметрах конструктора - додаючи модифікатор видимості.
// До PHP 8
class Invoice
{
private string $number;
private int $total;
public function __construct(string $number, int $total)
{
$this->number = $number;
$this->total = $total;
}
}
// PHP 8+
class Invoice
{
public function __construct(
private string $number,
private int $total,
) {}
}
Обидва варіанти створюють однакові властивості. Друге - втричі коротше, і неможливо забути присвоєння.
Що варто знати:
- Просувається лише параметр з модифікатором (
public,protected,private,readonly). Звичайні параметри поруч лишаються просто параметрами. - Можна поєднувати з
readonly:public function __construct(public readonly string $email) {}- незмінний value object у кілька рядків. - Тип обов'язковий не формально, але без нього властивість буде нетипізованою - на практиці тип пишуть завжди.
- Значення за замовчуванням дозволені:
private int $attempts = 3. - Тіло конструктора може бути й не порожнім - там зручно перевіряти інваріанти:
public function __construct(public readonly int $amount)
{
if ($amount < 0) {
throw new InvalidArgumentException('Сума не може бути від\'ємною');
}
}
У Laravel так оголошують залежності сервісів і контролерів: контейнер підставляє їх через параметри конструктора.
Звичайний метод працює з конкретним об'єктом і має доступ до $this - його стану.
Статичний метод належить класу, а не об'єкту: викликається без створення екземпляра (Money::fromCents(100)), і $this у ньому немає.
final class Money
{
private function __construct(private int $cents) {}
public static function fromCents(int $cents): self // статичний: створює об'єкт
{
return new self($cents);
}
public function add(Money $other): self // звичайний: працює зі станом
{
return new self($this->cents + $other->cents);
}
}
Коли static доречний:
- Іменовані конструктори й фабричні методи:
Money::fromCents(),Carbon::parse(). - Чисті допоміжні функції без стану, логічно прив'язані до класу:
Str::slug(). - Константи й кеш на рівні класу - обережно (див. нижче).
Чому зі статикою обережні:
- Важко підмінити в тестах. Код, що викликає
PaymentGateway::charge(), жорстко прив'язаний до класу; залежність, передана в конструктор, легко замінюється на фейк. - Статичні властивості - глобальний стан. Вони живуть увесь процес: у довгоживучих воркерах і Octane значення «перетікає» між запитами.
- Приховані залежності: зі сигнатури класу не видно, що він користується статикою іншого класу.
Фасади Laravel (Cache::get()) лише виглядають статичними: насправді це виклик методу об'єкта з контейнера, і в тестах його можна підмінити (Cache::fake(), Cache::shouldReceive()).
Трейт - це шматок коду (методи, властивості), який підставляється в клас так, ніби його написали там. Механізм повторного використання без успадкування: клас може взяти кілька трейтів.
trait HasSlug
{
public function slug(): string
{
return Str::slug($this->title);
}
}
class Post
{
use HasSlug;
}
Підводні камені:
- Неявні залежності. Трейт вище звертається до
$this->title, але ніде цього не оголошує. Клас безtitleзламається лише під час виконання. Частково рятує абстрактний метод у трейті:abstract public function title(): string;. - Конфлікти імен. Два трейти з однаковим методом - фатальна помилка, доки не розв'язати її через
insteadofіas. - Трейт - не тип. Не можна написати
function f(HasSlug $x): перевірити, що об'єкт має поведінку трейту, можна лише через інтерфейс. - Прихована складність. Клас із п'ятьма трейтами важко читати: щоб зрозуміти, що в ньому є, треба відкрити шість файлів.
Добре працюють трейти для вузької допоміжної поведінки, як SoftDeletes чи HasFactory у Laravel. Погано - як спосіб розкидати велику модель по файлах: складність лишається, вона просто гірше видна.
self:: завжди вказує на клас, у якому написано код. static:: - на клас, через який метод викликали насправді. Друге називають пізнім статичним зв'язуванням.
class Model
{
public static function create(): static
{
return new static(); // клас виклику
}
public static function make(): self
{
return new self(); // завжди Model
}
}
class User extends Model {}
User::create(); // User
User::make(); // Model
Де це важливо:
- Фабричні методи в базовому класі. Eloquent
User::create()повертає самеUser, бо всерединіnew static. - Перевизначені константи й методи.
static::TABLEвізьме константу нащадка,self::TABLE- базового класу. - Тип повернення
static(PHP 8.0) каже аналізатору й IDE, що метод повертає клас виклику - зручно для fluent-інтерфейсів.
Коли self: коли поведінка не має змінюватися в нащадках, наприклад виклик приватного методу. static:: до приватного методу нащадка не дістанеться.
Магічні методи - методи з подвійним підкресленням, які PHP викликає сам у певних ситуаціях:
__construct,__destruct- створення й знищення об'єкта;__get,__set,__isset,__unset- звернення до недоступної (неіснуючої чи приватної) властивості;__call,__callStatic- виклик недоступного методу;__toString- перетворення на рядок;__invoke- виклик об'єкта як функції:$validator($value);__clone- післяclone;__serialize,__unserialize- серіалізація;__debugInfo- що показувати уvar_dump().
__get і __call - основа «магії» Eloquent: $user->email читає атрибут з масиву, $user->posts() - зв'язок, User::where(...) через __callStatic перенаправляється в query builder.
Чим вони небезпечні:
- Опечатки не ловляться.
$user->emialне дасть помилки компіляції - лишеnullчи виняток під час виконання. - IDE й статичний аналіз сліпі. Автодоповнення, «перейти до визначення», перевірка типів - не працюють без додаткових підказок (
@property,@methodу PHPDoc, ide-helper, Larastan). - Складно налагоджувати: незрозуміло, звідки береться значення.
- Продуктивність: магічний виклик повільніший за прямий (рідко має значення, але в гарячих циклах помітно).
Правило: у власному коді віддавати перевагу явним методам і властивостям. Магію - лише там, де вона дає великий виграш у зручності API (як ORM), і з PHPDoc-підказками для інструментів.
final class- від класу не можна успадкуватися.final public function- метод не можна перевизначити в нащадку.final const(PHP 8.1) - константу не можна перевизначити.
final class InvoiceNumberGenerator
{
public function next(): string { /* ... */ }
}
class Report extends InvoiceNumberGenerator {} // Fatal error
Чому final за замовчуванням:
- Успадкування - найсильніший зв'язок. Нащадок залежить від усіх деталей предка. Будь-яка зміна в класі може зламати чужого нащадка, про якого автор навіть не знає.
- Свобода змінювати клас. Якщо клас
final, його внутрішню будову (protected-методи, порядок викликів) можна змінювати вільно: зовні доступний лише публічний API. - Свідомі рішення. Прибрати
final, коли з'явилася реальна потреба в успадкуванні, - одна правка. Повернутиfinal, коли на клас уже успадковуються, - ламаюча зміна. - Підштовхує до композиції й інтерфейсів замість ієрархій.
Типове заперечення - «а як мокати в тестах?». Мокати слід інтерфейси, а не конкретні класи. Якщо клас треба підмінити, - він реалізує інтерфейс, і тест підставляє іншу реалізацію. Для зовнішніх final-класів бібліотек є обхідні шляхи (пакет dg/bypass-finals), але це сигнал щодо дизайну.
Де final не ставлять: базові класи, призначені для успадкування (Model, Controller, абстрактні класи), і класи, які фреймворк проксує чи розширює під час виконання (наприклад, моделі Doctrine з лінивими проксі).
Успадкування - найсильніший зв'язок між класами: нащадок залежить від усіх деталей предка, зокрема захищених. Будь-яка зміна в базовому класі може тихо зламати десяток нащадків - це називають проблемою крихкого базового класу.
Типові симптоми:
- Ієрархія росте вглиб:
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) заборонене.