<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
АРХІТЕКТУРА · MIDDLE

Factory Method, Builder і model factories у Laravel: що з цього є що?

Factory Method відповідає на питання «який клас створити»: базовий код просить продукт у власного методу, а конкретний клас визначає підклас, як `createRedisDriver()` у нащадках `Illuminate\Support\Manager`. Builder відповідає на питання «як зібрати один складний обʼєкт»: кроки з назвами й перевірка всіх правил разом у `build()`. Model factories у Laravel з Factory Method мають спільну лише назву: за змістом це незмінний будівельник тестових даних.

Чим фабричний метод відрізняється від будівельника?
`User::factory()` у Laravel - це патерн Factory?
Конструктор платіжного доручення має дванадцять параметрів, і їх постійно плутають місцями. Що з цим робити?
Ми зібрали `$base = Vacancy::query()->where('active', true)`, потім `$remote = $base->where('remote', true)->get()`, а `$base->get()` теж повернув лише віддалені вакансії. Чому?
design patterns Factory Method Builder model factories Query Builder Laravel

Обидва патерни належать до породжувальних, і на співбесіді їх часто зводять до «щось, що створює обʼєкти». Корисніше розвести їх за питанням, на яке вони відповідають. Factory Method відповідає, який клас створити: базовий клас описує сценарій (надіслати клієнту посилання на оплату) і просить продукт у власного абстрактного методу, а підклас вирішує, чи це посилання monobank, чи LiqPay. Builder відповідає, як зібрати один обʼєкт, у якого багато частин і правила між ними: платіжне доручення з IBAN, сумою, ПДВ і призначенням збирається ланцюжком методів, а build() перевіряє все разом і віддає готовий обʼєкт.

У Laravel обидва патерни є, але не там, де їх шукають за назвою. Справжній Factory Method живе в Illuminate\Support\Manager: createDriver() шукає в нащадку метод create{Name}Driver(), і так SessionManager створює файловий чи database-драйвер. А Vacancy::factory() з Factory Method повʼязує лише слово: definition(), state(), count(), for() і has() збирають опис тестових записів, make() чи create() їх видають, тобто це будівельник. Будівельниками є і Query Builder, і MailMessage у сповіщеннях, а в Symfony FormBuilderInterface і Doctrine QueryBuilder.

На практиці найчастіше ламається мутабельність. Query Builder змінює сам себе: where() додає умову до того самого обʼєкта й повертає його ж, тому базовий запит, з якого зробили дві гілки, отримує умови обох. Перед розгалуженням потрібен clone() або новий виклик query(). Model factory поводиться навпаки: кожен state() і count() повертає нову копію, тож базовий опис можна тримати в змінній і використовувати повторно.

Межі в обох патернів помітні. Підклас, який існує лише заради одного new, простіше замінити переданим у конструктор обʼєктом чи замиканням: композицію легше читати й тестувати, ніж ієрархію. Будівельник для обʼєкта з кількома полями без правил між ними поступається named arguments з PHP 8, а для типових варіантів ще краще читаються іменовані конструктори на кшталт PaymentOrder::salary().

// Builder: обʼєкт збирається кроками з назвами, правила перевіряє build()
final class PaymentOrderBuilder
{
    private ?string $iban = null;
    private ?int $amount = null;
    private ?int $vat = null;
    private string $purpose = '';

    public function to(string $iban): self { $this->iban = $iban; return $this; }
    public function amount(int $kopecks): self { $this->amount = $kopecks; return $this; }
    public function withVat(int $kopecks): self { $this->vat = $kopecks; return $this; }
    public function purpose(string $text): self { $this->purpose = $text; return $this; }

    public function build(): PaymentOrder
    {
        if ($this->iban === null || $this->amount === null) {
            throw new LogicException('Потрібні IBAN і сума');
        }
        // Правило на кілька полів: перевірити його можна, лише коли зібрано все
        if ($this->vat !== null && $this->vat >= $this->amount) {
            throw new LogicException('ПДВ не може дорівнювати сумі чи перевищувати її');
        }

        return new PaymentOrder($this->iban, $this->amount, $this->vat, $this->purpose);
    }
}

// Factory Method: сценарій у базовому класі, а клас продукту обирає підклас
abstract class PaymentLinkSender
{
    abstract protected function createLink(Order $order): PaymentLink;

    public function message(Order $order): string
    {
        return "Оплатіть замовлення №{$order->id}: {$this->createLink($order)->url()}";
    }
}

final class MonobankLinkSender extends PaymentLinkSender
{
    protected function createLink(Order $order): PaymentLink { return new MonobankLink($order); }
}
Що кандидат розводить патерни за питанням: Factory Method вибирає клас продукту, Builder збирає один обʼєкт з частин.
Що він чесно відрізняє Factory Method з GoF (вибір у підкласі) від простої фабрики (статичний метод з `match`) і не вважає це помилкою, а лише різними назвами.
Що правила між полями будівельник перевіряє в `build()`, а не в кожному сеттері.
Що він знає про мутабельність: Laravel Query Builder змінює себе і для розгалуження потребує `clone()`, а `state()` і `count()` у model factory повертають нову копію.
Що в PHP 8 часто вистачає named arguments або іменованого конструктора, і окремий будівельник потрібен не завжди.
Називати фабричним методом GoF будь-який статичний `create()` з `match` усередині: це проста фабрика, а у Factory Method вибір робить підклас, і базовий клас працює з продуктом через інтерфейс.
Перевіряти правила в кожному сеттері будівельника: поки обʼєкт збирається, частини полів ще немає, тому правило на кілька полів (ПДВ менший за суму) там перевірити неможливо.
Використати один `Vacancy::query()->where(...)` як основу для двох запитів: `where()` змінює той самий білдер і повертає його ж, тож другий запит отримує умови першого.
Вважати `User::factory()` продакшн-фабрикою й ховати в `definition()` правила створення сутності: model factory існує для тестів і сидерів, а застосунок створює сутності через доменні методи або сервіси.
Писати будівельник для обʼєкта з трьома полями без правил між ними, де named arguments читаються так само і не вимагають ще одного класу.
ПОРАДА

Скажіть: «Factory Method вирішує, який клас, Builder вирішує, як зібрати». Потім покажіть, що знаєте про мутабельність: Query Builder треба клонувати перед розгалуженням, а model factory можна тримати як шаблон, бо кожен `state()` повертає нову копію.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Factory Method стосується вибору класу: базовий код просить продукт у власного методу, а що саме створити, вирішує підклас. Builder стосується складання одного обʼєкта з частин. Будівельник буває і мутабельним, як Laravel Query Builder, і незмінним, як model factory, а `Vacancy::factory()` за змістом саме будівельник тестових даних.