Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «ООП»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Трейт - це шматок коду (методи, властивості), який підставляється в клас так, ніби його написали там. Механізм повторного використання без успадкування: клас може взяти кілька трейтів.

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 з лінивими проксі).

Докладніше в документації: Ключове слово final