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

Laravel Rulebook: Бізнес-Правила, Що Змінюються з Часом

Mathias Onea випустив Laravel Rulebook - пакет для роботи з бізнес-правилами, які змінюються з часом. Правила визначаються як звичайні PHP-класи, а їх розв'язання повертає єдиного переможця на конкретний момент часу разом із обґрунтуванням рішення.

Пакет призначений для рішень, які потрібно пояснити постфактум. Наприклад, відшкодування, розраховане в березні за умовами, які вже змінилися, або рахунок, виставлений за минулорічною ставкою комісії. Якщо політику редагували на місці, єдиний запис про те, як працював код раніше, - це історія git, а її не вставиш у відповідь клієнту.

Основні Можливості

  • Кожне правило оголошує вікно свого застосування через always(), from(), until() або between(), тому торішня політика залишається в кодовій базі поруч із цьогорічною. Вікна напіввідкриті, тому послідовні роки ніколи не перекриваються.
  • Розв'язання повертає рівно одного переможця, обраного за priority(), а не за позицією в масиві правил.
  • Кожне правило повертається зі статусом та причиною - чи воно перемогло, програло, або опинилося поза своїм вікном. Причини можуть містити reasonCode для фільтрації.
  • Коли рішення прийняти неможливо, викидається виняток: NoMatchingRule, коли нічого не підходить, та AmbiguousRuleMatch, коли два правила отримують однаковий пріоритет.
  • snapshot() може заморозити рішення в JSON-запис, який можна зберегти разом із відповідним записом.
  • Правила беруться з service container, а rulebook є узагальненим над своїм subject, context та outcome.
  • Без фасадів, реєстру, конфігураційного файлу чи міграцій.

Пакет вимагає PHP 8.3 та Laravel 12 або 13:

composer require mathiasonea/laravel-rulebook

Створення Rulebook для Відшкодувань

Розглянемо платформу для подій. До кінця 2025 року гнучкий квиток повністю відшкодовувався за умови повідомлення за сім днів. З січня 2026 року термін повідомлення зріс до чотирнадцяти днів, а також було введено збір за обробку $3.50. Ще два правила мають нижчий пріоритет: відшкодування доброї волі в розмірі половини вартості квитка при скасуванні більш ніж за 30 днів, та правило за замовчуванням - відсутність відшкодування.

Річні політики поділяють перевірки придатності, тому батьківський клас містить логіку, а кожен рік надає власні числа:

abstract class FlexibleFareRefund extends Rule
{
    public function priority(): int
    {
        return 100;
    }

    public function evaluate(RuleInput $input): RuleResult
    {
        $ticket = $input->subject(Ticket::class);
        $cancellation = $input->context(Cancellation::class);

        if ($cancellation->fare !== 'flexible') {
            return RuleResult::doesNotApply(
                reason: 'The ticket was sold on a saver fare.',
                reasonCode: 'fare_not_flexible',
            );
        }

        if ($cancellation->daysBeforeEvent < $this->noticeInDays()) {
            return RuleResult::doesNotApply(
                reason: "A flexible fare needs {$this->noticeInDays()} days of notice.",
                reasonCode: 'insufficient_notice',
            );
        }

        return RuleResult::applies(
            outcome: new Refund($ticket->priceInCents - $this->handlingFeeInCents()),
            reason: "Refunded under the {$this->policyYear()} flexible fare policy.",
        );
    }

    abstract protected function policyYear(): int;
    abstract protected function noticeInDays(): int;
    abstract protected function handlingFeeInCents(): int;
}

Правило 2025 року закривається першого січня, а правило 2026 року відкривається в той самий момент:

final class FlexibleFareRefund2025 extends FlexibleFareRefund
{
    public function validity(): ValidityPeriod
    {
        return ValidityPeriod::between(
            from: new DateTimeImmutable('2025-01-01T00:00:00-05:00'),
            until: new DateTimeImmutable('2026-01-01T00:00:00-05:00'),
        );
    }

    protected function policyYear(): int { return 2025; }
    protected function noticeInDays(): int { return 7; }
    protected function handlingFeeInCents(): int { return 0; }
}

final class FlexibleFareRefund2026 extends FlexibleFareRefund
{
    public function validity(): ValidityPeriod
    {
        return ValidityPeriod::from(new DateTimeImmutable('2026-01-01T00:00:00-05:00'));
    }

    protected function policyYear(): int { return 2026; }
    protected function noticeInDays(): int { return 14; }
    protected function handlingFeeInCents(): int { return 350; }
}

Сам rulebook лише перераховує правила:

/** @extends Rulebook<Ticket, Cancellation, Refund> */
final class RefundRulebook extends Rulebook
{
    protected function rules(): array
    {
        return [
            NoRefund::class,
            GoodwillRefund::class,
            FlexibleFareRefund2025::class,
            FlexibleFareRefund2026::class,
        ];
    }
}

Використання Rulebook

Тепер запитаємо відшкодування для квитка вартістю $89.00, скасованого за десять днів, у листопаді 2025 року:

$decision = $rulebook->resolveAt(
    subject: new Ticket(reference: 'TCK-4193', priceInCents: 89_00),
    at: new DateTimeImmutable('2025-11-02T09:00:00-05:00'),
    context: new Cancellation(fare: 'flexible', daysBeforeEvent: 10),
);

$decision->outcome()->formatted();          // $89.00
class_basename($decision->winningRule());   // FlexibleFareRefund2025
$decision->winningResult()->reason();       // Refunded under the 2025 flexible fare policy.

Змініть дату на 2026 рік, і відшкодування повернеться як $0.00, оскільки десять днів менше чотирнадцяти днів, які вимагає новіша політика. Більше нічого у виклику не змінилося.

Ви також отримуєте решту оцінки. Кожне правило, яке було розглянуто, має статус та причину. Наприклад, NoRefund має статус applicable, GoodwillRefund - does_not_apply з причиною "Скасування всередині 30-денного вікна доброї волі", FlexibleFareRefund2025 - outside_validity з причиною "Правило недійсне на 2026-11-02", а FlexibleFareRefund2026 - does_not_apply з причиною "Гнучкий тариф потребує 14 днів повідомлення".

NoRefund перемагає, оскільки це останнє правило, що залишилося, а таблиця пояснює чому. Правило з міткою outside_validity було пропущено без виконання evaluate(), що дозволяє відрізнити "ця політика ще не існувала" від "ця політика переглянула квиток і сказала ні".

Збереження Рішень

Щоб зберегти цей запис, викличте snapshot() на рішенні. Скаляри, масиви, backed enum та JsonSerializable outcomes проходять як є, а все інше потребує callback:

$snapshot = $decision->snapshot(
    normalizeOutcome: static fn (Refund $r): array => ['amount_in_cents' => $r->amountInCents],
);

$refund->update(['policy_snapshot' => json_encode($snapshot)]);

Snapshot реалізує JsonSerializable, тому ви можете використовувати json_encode() на ньому. Ви також можете використовувати toArray(), коли стовпець бази даних має cast array на моделі, і ви хочете, щоб Eloquent закодував його при збереженні.

Ось скорочений запис для відшкодування 2025 року:

{
  "schema_version": 1,
  "evaluated_at": "2025-11-02T09:00:00.000000-05:00",
  "winning_rule_key": "App\\FlexibleFareRefund2025",
  "outcome": { "amount_in_cents": 8900 },
  "evaluations": [
    {
      "key": "App\\GoodwillRefund",
      "rule_class": "App\\GoodwillRefund",
      "priority": 50,
      "valid_from": null,
      "valid_until": null,
      "status": "does_not_apply",
      "reason": "The cancellation is inside the 30 day goodwill window.",
      "reason_code": "inside_goodwill_window"
    }
  ]
}

Зверніть увагу на поле key. За замовчуванням воно відповідає імені класу, тому якщо ви перейменуєте правило, ідентифікатор зміниться в кожному вже збереженому записі. Дайте кожному правилу власний key(), щось на кшталт refunds.flexible-fare.2026, до того, як будь-які з них потраплять до бази даних.

Коли Не Використовувати Rulebook

Для однієї перевірки дати в одному сервісі вираз match буде зрозумілішим, ніж чотири класи та rulebook. Використовуйте Rulebook, коли хтось запитає про те саме рішення через місяці, і вам потрібно показати, як ви дійшли до числа.

Немає DSL, немає правил, збережених у базі даних або відредагованих через адмін-панель, немає workflow або поведінки state machine, і немає результату, складеного з кількох переможців. Розв'язання старої дати відтворює політику так, як її виражають сьогоднішні класи, що не є повторним відтворенням оригінального виконання, тому це менше, ніж повний audit trail.

Вихідний код та приклад застосунку доступні на GitHub разом із повною документацією.

28

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

Новини 27 жовтня 2026

Filament v5.11.0: поля й віджети на React, Vue і Svelte та жорсткіший захист Livewire

Filament v5.11.0 дозволяє писати власні поля форм, компоненти схем і віджети на React, Vue, Svelte або чистому JavaScript. Паралельно v5.11.0 і v4.16.0 закривають для клієнта внутрішні Livewire-методи та стан і додають опційні ліміти для репітерів і пагінації таблиць.

2
Новини 27 жовтня 2026

Livewire 4.4.8 закриває обхід #[Authorize], а NativePHP Mobile - сім вразливостей

Livewire 4.4.8 виправляє GHSA-mhf7-xp6r-6qrc (CVSS 6.5): атрибут #[Authorize] можна було обійти й виконати дію над чужим записом. Того ж тижня NativePHP опублікувала сім попереджень для NativePHP for Mobile - виправлення в nativephp/mobile 4.6.1 і 3.3.11, але до користувачів вони дійдуть лише з новою збіркою застосунку.

1

Вакансії за темою

Middle Backend Engineer PHP/Laravel

Розробка бекенду для нової платформи запису й редагування відео з AI: REST API для керування контентом (S3-завантаження, метадані, мініатюри, планування), оркестрація задач обробки відео на AI-воркерах RunPod, інтеграції з платежами, аналітикою та AI-сервісами, WebSocket-синхронізація з клієнтами та фонові черги. Потрібен досвід із бекенд-фреймворками, проєктуванням баз даних, автентифікацією й безпекою API, роботою з вебхуками та Docker, Git і CI/CD.

Senior/Middle PHP Developer

Підтримка та розвиток великих веб-сайтів компанії (понад 1 млн відвідувань на місяць) на Laravel та OctoberCMS, інтеграція з внутрішніми системами та платіжними шлюзами (Приват24, Portmone, EasyPay, LiqPay), оптимізація продуктивності та робота з великим legacy-кодом без ризику для продакшену. Потрібен досвід PHP від 4 років, впевнена робота з Laravel і OctoberCMS, знання HTML5, CSS3 та JavaScript, вища освіта.

SoftHouseGroup Нова
2 дні тому

PHP Software Engineer

Розробник буде частиною full-stack команди, яка розвиває велику складну систему у страховому домені з багатьма модулями та інтеграціями з платіжними провайдерами. Щодня потрібні розробка нових модулів, підтримка існуючих, виправлення багів та перевірка коду, згенерованого AI. Ключові вимоги: понад 4 роки досвіду PHP, Laravel або Yii2, знання ООП і патернів проєктування, MySQL, REST API, фронтенд (HTML, CSS, JavaScript, jQuery, Vue.js), Git і англійська на рівні Pre-Intermediate для читання.

Пакети за темою

Kalimera

welldigit/kalimera

Interactive installer that scaffolds Laravel applications the Well Digit way.

v1.0.0 15

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,282 v2.5.0 13 26