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

PHP: Middle

20 питань · ~18 хв · Версія v3.0

Увійдіть, щоб продовжити

ООП, traits, замикання, генератори, винятки, SPL.

За спробу
20
У пулі
100
Проходжень
40
Середній бал
84%
Пройшли на 70%+
80%

Питання для підготовки

35 питань

Два способи оголосити глобальну константу:

const MAX_UPLOAD_MB = 20;              // на етапі компіляції
define('MAX_UPLOAD_MB', 20);           // під час виконання, звичайна функція
const define()
коли визначається під час компіляції файлу під час виконання
усередині if, циклу, функції ні так
ім'я з виразу ні так: define("LIMIT_{$plan}", 10)
у класах і інтерфейсах так ні
простір імен враховує поточний namespace ім'я задається повністю рядком

На практиці в сучасному коді define() майже не потрібен: глобальні константи взагалі краще замінити конфігурацією чи константами класу.

Константи класу - значення, що належать класу й не змінюються:

final class Invoice
{
    public const int PAYMENT_TERM_DAYS = 14;
    protected const string NUMBER_PREFIX = 'INV-';

    public function dueDate(): CarbonImmutable
    {
        return $this->issued_at->addDays(self::PAYMENT_TERM_DAYS);
    }
}
  • видимість (public, protected, private) з PHP 7.1;
  • типізовані константи (const int) з PHP 8.3 - нащадок не може перевизначити константу значенням іншого типу;
  • final const (PHP 8.1) - заборонити перевизначення в нащадках;
  • static::CONST замість self::CONST - взяти значення з класу-нащадка (пізнє статичне зв'язування).

Магічні значення - числа й рядки без пояснення посеред коду:

if ($user->status === 3 && $order->total > 50000) { ... }   // що таке 3? чому 50000?

Замінювати їх варто так:

  • набір пов'язаних варіантів (статуси, типи, ролі) - enum, а не група констант: тип перевіряється, варіанти можна перебрати, у них можуть бути методи;
  • одиночне незмінне значення предметної області (термін оплати, максимальна довжина) - константа класу, де воно використовується;
  • значення, що відрізняється між середовищами чи може змінитися без деплою (ліміти, ключі, адреси) - конфігурація (config('billing.limit')), а не константа.

Константи в інтерфейсах допустимі, але вони потрапляють у кожен клас, що реалізує інтерфейс. Інтерфейс описує поведінку, тож набір значень для нього - ознака, що потрібен enum.

Динамічний доступ: constant(Invoice::class . '::PAYMENT_TERM_DAYS') чи з PHP 8.3 Invoice::{$name} - корисно в рідкісних випадках, але ламає пошук використань в IDE.

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

Backed enum кастується в моделі, і колонка починає повертати обʼєкт замість рядка:

enum VacancyLevel: string
{
    case Junior = 'junior';
    case Middle = 'middle';
    case Senior = 'senior';

    public function label(): string
    {
        return match ($this) {
            self::Junior => 'Junior',
            self::Middle => 'Middle',
            self::Senior => 'Senior',
        };
    }
}

class Vacancy extends Model
{
    protected function casts(): array
    {
        return ['level' => VacancyLevel::class];
    }
}

Тепер $vacancy->level - це enum, а не рядок:

$vacancy->level->label();
$vacancy->level === VacancyLevel::Senior;
$vacancy->update(['level' => VacancyLevel::Middle]);   // у базу піде 'middle'

Переваги над константами класу:

  • Обмежена множина. Значення поза переліком не існує, тоді як константа не заважає передати будь-який рядок.
  • Типізація. function assign(VacancyLevel $level) не прийме випадковий рядок, і редактор підкаже варіанти.
  • Поведінка поруч зі значенням. Enum має методи, тож підпис, колір чи іконка живуть там само, а не в розкиданих match по шаблонах.
  • match без default підсвітить пропущений випадок, коли додасте новий кейс.

Дві практичні деталі. У валідації є правило Rule::enum(VacancyLevel::class). А tryFrom() повертає null замість винятку - саме він потрібен, коли значення приходить від користувача чи з URL.

Докладніше в документації: Eloquent: мутатори та касти

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

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:: до приватного методу нащадка не дістанеться.

Докладніше в документації: Пізнє статичне зв'язування

Обидва об'єднують масиви, але по-різному поводяться з однаковими ключами:

  • $a + $b - бере все з $a, а з $b додає лише ключі, яких у $a немає. Перемагає лівий.
  • array_merge($a, $b) - для рядкових ключів перемагає правий, а числові ключі перенумеровує з нуля й дописує в кінець.
$defaults = ['color' => 'red', 'size' => 'M'];
$options  = ['color' => 'blue'];

$options + $defaults;            // ['color' => 'blue', 'size' => 'M']
array_merge($defaults, $options); // ['color' => 'blue', 'size' => 'M']

[0 => 'a', 1 => 'b'] + [0 => 'c', 1 => 'd', 2 => 'e']; // ['a', 'b', 'e']
array_merge(['a', 'b'], ['c', 'd', 'e']);              // ['a', 'b', 'c', 'd', 'e']

Коли що:

  • + - накласти значення за замовчуванням: «мої опції, а чого бракує - з дефолтів».
  • array_merge або spread [...$a, ...$b] - склеїти списки.

Пастка: числові ключі, які насправді ідентифікатори ([15 => 'Київ']), array_merge перенумерує й загубить. Для таких масивів потрібен + або array_replace.

Докладніше в документації: array_merge

Прочитати - ще не значить знати

20 питань, по одному на екран, ~18 хв. Після завершення - розбір кожної помилки з посиланням на питання.