Laravel Rulebook: Time-Based Business Rules in PHP | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. [Laravel](https://www.msaied.com/public/articles?category=laravel)
6. /
7. Laravel Rulebook: Manage Business Rules That Change by Date

   [Laravel](https://www.msaied.com/public/articles?category=laravel) [Composer Pacakge](https://www.msaied.com/public/articles?category=composer-pacakge) 

 Laravel Rulebook: Manage Business Rules That Change by Date
============================================================

 Laravel Rulebook is a package that lets you model time-bound business rules as plain PHP classes, resolve exactly one winner at any point in time, and snapshot the full decision for later explanation.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 7 Sep 2026 · Updated 7 Sep 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Rulebook: Manage Business Rules That Change by Date](https://cdn.msaied.com/640/abbdec8836738d39a6650923a6b6ca5e.png) 

  On this page +1. [What Is Laravel Rulebook?](#what-is-laravel-rulebook)
2. [Key Features](#key-features)
3. [Building a Time-Versioned Refund Policy](#building-a-time-versioned-refund-policy)
4. [Storing Decisions with Snapshots](#storing-decisions-with-snapshots)
5. [When Not to Use Rulebook](#when-not-to-use-rulebook)
6. [Real Takeaways](#real-takeaways)

 What Is Laravel Rulebook?
-------------------------

[Laravel Rulebook](https://laravel-news.com/laravel-rulebook) by Mathias Onea is a package for encoding business rules that change over time. Each rule is a plain PHP class with a declared validity window. When you resolve a rulebook at a specific date, you get exactly one winning rule, the outcome it produced, and a full explanation of every rule that was considered.

The core use case is decisions that must be explained after the fact — a refund calculated under last March's terms, or an invoice raised at last year's commission rate. Editing a policy in place destroys that history; Rulebook keeps every version of the policy alive in the codebase.

Key Features
------------

- **Validity windows** — declare a rule's active period with `always()`, `from()`, `until()`, or `between()`. Windows are half-open, so consecutive periods never overlap.
- **Priority-based resolution** — exactly one winner is chosen by `priority()`, never by array position.
- **Rich result objects** — every rule returns a status (`applicable`, `does_not_apply`, `outside_validity`) and a human-readable reason with an optional `reasonCode`.
- **Explicit failure modes** — throws `NoMatchingRule` when nothing applies, and `AmbiguousRuleMatch` when two rules tie.
- **Snapshots** — freeze a decision into a JSON-serializable record you can store alongside the data it governs.
- **No magic** — no facade, no config file, no migration, no database-stored rules.

Requires PHP 8.3 and Laravel 12 or 13:

```bash
composer require mathiasonea/laravel-rulebook

```

Building a Time-Versioned Refund Policy
---------------------------------------

Consider an events platform with two flexible-fare refund policies. Until end of 2025: full refund with 7 days' notice, no fee. From January 2026: 14 days' notice required and a $3.50 handling fee.

A shared abstract class holds the evaluation logic; each year's rule supplies its own numbers and validity window:

```php
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 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 noticeInDays(): int        { return 14; }
    protected function handlingFeeInCents(): int  { return 350; }
}

```

Resolving at a specific point in time is a single call:

```php
$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

```

Shift the date to 2026 and the same call returns $0.00 — ten days falls short of the 2026 policy's fourteen-day requirement. The `FlexibleFareRefund2025` rule shows status `outside_validity`, making it clear the policy simply did not exist at that date rather than actively rejecting the claim.

Storing Decisions with Snapshots
--------------------------------

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

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

```

The snapshot is `JsonSerializable` and also exposes `toArray()` for Eloquent models with an `array` cast. One important detail: the rule identifier defaults to the class name. Rename a class and every stored snapshot loses its link. Assign a stable `key()` — such as `refunds.flexible-fare.2026` — before any snapshots reach a database.

When Not to Use Rulebook
------------------------

A single date check in one service is clearer as a `match` expression. Rulebook pays off when:

- Decisions span multiple policy eras.
- Someone will ask months later why a specific number was calculated.
- You need a stored, human-readable explanation alongside the record.

It is not a DSL, not a workflow engine, and not a full audit trail — resolving an old date reproduces the policy as today's classes express it, not a replay of the original execution.

Real Takeaways
--------------

- Keep every policy version in code; validity windows prevent overlap without extra logic.
- `resolveAt()` makes time-travel testing trivial — change one argument, get a different winner.
- The full evaluation table (not just the winner) is available on every decision object.
- Assign stable `key()` values to rules before persisting any snapshots.
- The package has zero infrastructure requirements: no migrations, no config, no facade.

---

Source: [Laravel Rulebook: Business Rules That Change by Date — Laravel News](https://laravel-news.com/laravel-rulebook)

- [Laravel](https://www.msaied.com/public/articles?search=Laravel)
- [PHP](https://www.msaied.com/public/articles?search=PHP)
- [Business Rules](https://www.msaied.com/public/articles?search=Business%20Rules)
- [Composer Package](https://www.msaied.com/public/articles?search=Composer%20Package)
- [Policy Versioning](https://www.msaied.com/public/articles?search=Policy%20Versioning)

 Frequently asked questions 
---------------------------

  How does Laravel Rulebook handle two rules with overlapping validity periods?Validity windows are half-open intervals, so a rule closing on 2026-01-01 and a rule opening on 2026-01-01 never overlap. If two rules do match the same point in time and share the same priority, Rulebook throws an `AmbiguousRuleMatch` exception rather than silently picking one.

   Can I replay an old decision exactly as it was originally executed?Not exactly. Resolving at a past date reproduces the policy as today's classes express it. If you have since changed the logic inside a rule class, the re-resolution will use the updated code. For a true replay you need to store a snapshot at decision time and read that back instead.

   What happens if I rename a rule class after snapshots have been stored?By default the rule's identifier is its fully-qualified class name, so renaming the class breaks the link in every stored snapshot. To avoid this, override the `key()` method on each rule with a stable string such as `refunds.flexible-fare.2026` before any snapshots reach the database.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleFilament v4 Schema-Based Forms: Unified Schema API and Infolist Patterns](https://www.msaied.com/public/articles/filament-v4-schema-based-forms-unified-schema-api-and-infolist-patterns) [Next articleFind Unexpected Test Inputs with Fuzz for Pest](https://www.msaied.com/public/articles/find-unexpected-test-inputs-with-fuzz-for-pest)  

   On this page
-------------

1. [What Is Laravel Rulebook?](#what-is-laravel-rulebook)
2. [Key Features](#key-features)
3. [Building a Time-Versioned Refund Policy](#building-a-time-versioned-refund-policy)
4. [Storing Decisions with Snapshots](#storing-decisions-with-snapshots)
5. [When Not to Use Rulebook](#when-not-to-use-rulebook)
6. [Real Takeaways](#real-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
