Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Period Laravel Package

league/period

League\Period is PHP's missing time range API, offering immutable Interval and Bounds value objects, named constructors, and full support for simple or complex time range logic. Framework-agnostic, documented, and requiring PHP 8.1+.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect the version first. This package has been through several major renames, and almost every "why doesn't this method exist" question is a version question:

composer require league/period
composer show league/period   # note the version before copying snippets
  • 4.x and earlier: the main class is League\Period\Period; boundary behaviour is set with integer constants like Period::INCLUDE_START_EXCLUDE_END, and splitting is split()/splitForward().
  • 5.x+: the value object is Interval with a Bounds enum (Bounds::IncludeStartExcludeEnd, etc.), and Period remains mainly as a backwards-compatibility alias. If a snippet from a blog post fails, check whether it uses Period:: where your installed version expects Interval::.

Where to look first, in order:

  1. README.md — install + a one-line example; it deliberately stays short.
  2. The docs site (period.thephpleague.com) — this is where the API is actually explained; the README is not a tutorial.
  3. src/ — the classes are small and readable (Interval/Period, Datepoint/DatePoint, Duration, Sequence, the Bounds enum). Reading Interval end-to-end takes 15 minutes and answers most questions faster than searching.
  4. CHANGELOG.md — your upgrade guide. Deprecations here are the single biggest source of confusion (getDatePeriod → dateRangeForward, getDateInterval → dateInterval, sameValueAs → equals, createFrom* → named constructors).

First use case — booking/slot validation. This is what most teams actually pull the library in for: deciding whether a requested time range sits inside opening hours, collides with existing bookings, or abuts the previous one cleanly.

use League\Period\Period;          // Interval on 5.x
use League\Period\Bounds;

$openingHours = Period::fromDateRange('2024-03-15 09:00', '2024-03-15 17:00');
$requested    = Period::fromDateRange('2024-03-15 16:00', '2024-03-15 18:00');
$existing     = Period::fromDateRange('2024-03-15 10:00', '2024-03-15 11:30');

$openingHours->contains($requested->startDate); // true
$requested->overlaps($existing);                // false
$requested->overlaps($openingHours);            // true -> reject
$requested->isAfter($openingHours);             // false

Default boundary semantics are half-open, [start, end) — that single fact drives most of the gotchas below.

Implementation Patterns

1. Named constructors instead of hand-built ranges. Calendar units are first-class and timezone-aware; don't reimplement month/quarter boundaries with date() maths.

use League\Period\Datepoint;

Datepoint::fromDateString('2024-03-15')->month()->toIso80000('Y-m-d');
// [2024-03-01, 2024-04-01)
Datepoint::fromDateString('2024-03-15')->quarter();
Datepoint::fromDateString('2024-03-15')->isoWeek();
Datepoint::fromDateString('2024-03-15')->semester();

Relative constructors cover the "window around a point" cases cheaply: Period::after($start, $duration), Period::before($end, $duration), Period::around($point, $duration), Period::fromDatepoint($point, $duration).

2. Model billing/reporting buckets with splitForward(). Reporting that "buckets by day" is exactly a split, and the generator keeps memory flat for long ranges.

foreach ($month->splitForward(Duration::fromDateString('1 day')) as $day) {
    $counts[$day->startDate->format('Y-m-d')] = $this->countEvents($day);
}

3. Use immutable Periods as value objects in your domain. They're safe to put in DTOs, cache payloads, and collections because every mutator returns a new instance. A common pattern is a small wrapper:

final class BillingWindow
{
    public function __construct(public readonly Period $period) {}
    public function prorate(int $totalDays): float { /* ... */ }
}

4. Prefer Sequence over manual array juggling. For "free slots", "merged availability", "total booked time", Sequence is the shortest correct code:

use League\Period\Sequence;

$booked = new Sequence(...$periods);
$free   = (new Sequence($openingHours))->subtract($booked);   // gaps
$total  = $booked->totalTimeDuration();                        // absolute seconds

5. Laravel/Eloquent integration. Two patterns carry most of the weight.

Query scopes with SQL overlap — mirror the half-open semantics exactly:

public function scopeOverlapping(Builder $q, Period $period): Builder
{
    return $q->where('starts_at', '<', $period->endDate)   // strictly before end
             ->where('ends_at', '>', $period->startDate);  // strictly after start
}

Hydrate a Period from two columns via a custom cast:

public function get(Period $model, string $key, mixed $value, array $attributes): ?Period
{
    return isset($attributes['starts_at'], $attributes['ends_at'])
        ? Period::fromDateRange($attributes['starts_at'], $attributes['ends_at'])
        : null;
}

6. Validate, don't silently clamp. Turn overlap logic into a form request rule: reject when the incoming range overlaps() any existing one. Keeps the domain rules in one place and out of controllers.

7. Serialize at the edges only. toIso8601() for machine output, toIso80000($format) for human-facing strings, and jsonSerialize() (JS-compatible datepoint notation) when the Period is returned from an API resource. Keep DB storage as two timestamp columns — the library is not an ORM type.

Gotchas and Tips

  • Half-open by default. contains($end) is false, and a zero-duration Period contains nothing at all — not even its start. If you need inclusive reporting, be explicit: boundedBy(Bounds::IncludeAll) (or withBoundaryType(...) on 4.x). Never rely on the default matching a user's mental "9am to 5pm inclusive".
  • SQL BETWEEN is the classic mismatch. BETWEEN is inclusive; Period is not. The < / > pair shown above is the correct translation and also prevents double-counting an appointment touching the boundary.
  • Everything is immutable. $period->boundedBy($bounds); on its own does nothing. Most "it's not working" bug reports are an unassigned return value — same for gap(), intersect(), expand().
  • DateTimeImmutable everywhere. Inputs are normalised; mutable DateTime objects don't get mutated behind your back, and mutating your own object after building a Period will not affect the Period.
  • Timezone and DST. The library normalises to DateTimeImmutable but does not convert endpoints for you — construct both ends in the same timezone. A Duration is calendar-aware (DateInterval), so timeDuration() (absolute seconds) and dateInterval() can disagree across a DST change. Use timeDuration() for money and metrics, dateInterval() for calendar arithmetic. Splitting a range that spans a DST switch is the historical bug cluster here — pin a version and test that case explicitly.
  • intersect() on disjoint periods. Guard with overlaps() first; depending on version and shape this returns null or throws. Same for gap().
  • equals() ≠ ===. Equality compares value and boundary type. Two Periods with identical timestamps but different Bounds are not equal and will sort/collapse differently.
  • splitForward() returns a Generator. It's single-pass — iterator_to_array() to reuse it, and don't assume array functions work on it.
  • Version drift in method names. split/splitForward, getDatePeriod/dateRangeForward, format/toIso80000, withBoundaryType/boundedBy, Period::INCLUDE_*/Bounds::*. Deprecated aliases usually still work but will drown your static analysis in warnings; search-and-replace on a major bump is cheaper than tolerating them.
  • Extension points are deliberately thin. Period/Interval are effectively final in modern versions; extend by composition (wrapper class holding the Period) rather than subclassing. The natural seams are your own DTOs, casts, and query scopes.
  • Debugging tip: dump $period->toIso8601() rather than the object — it shows the boundary notation and endpoints, which is where 90% of the confusion lives ([2024-03-15T00:00:00+00:00, 2024-03-16T00:00:00+00:00)).
  • Don't use it as a stored type. It has no DB serialization contract; two columns plus a cast at the boundary is more portable than serializing the object into JSON and trying to reconstruct boundary types later.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc