PHPackages                             puntodev/bookables - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. puntodev/bookables

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

puntodev/bookables
==================

Bookable Library

v5.0.1(4d ago)07.1kMITPHPPHP &gt;=8.4 &lt;9.0CI passing

Since Sep 2Pushed 2w ago1 watchersCompare

[ Source](https://github.com/puntodev/bookables)[ Packagist](https://packagist.org/packages/puntodev/bookables)[ Docs](https://github.com/puntodev/bookables)[ RSS](/packages/puntodev-bookables/feed)WikiDiscussions master Synced today

READMEChangelog (10)Dependencies (14)Versions (23)Used By (0)

Bookables
=========

[](#bookables)

[![Latest Version on Packagist](https://camo.githubusercontent.com/af5f4083eb921ade899107afb8e3edd948a64b7ff691e393abacceb62ba8debc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f70756e746f6465762f626f6f6b61626c65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/puntodev/bookables)[![Build Status](https://camo.githubusercontent.com/21efd80e7785e646a6451ea01c1f4a59e79e772c1e7cde24c510a155a5873fbc/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f70756e746f6465762f626f6f6b61626c65732f7068702e796d6c3f6272616e63683d6d6173746572267374796c653d666c61742d737175617265)](https://github.com/puntodev/bookables/actions/workflows/php.yml)[![Total Downloads](https://camo.githubusercontent.com/335c75b871093906255505ea61d529c7607b6b5e27b3545162b24299790c11cd/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f70756e746f6465762f626f6f6b61626c65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/puntodev/bookables)[![License](https://camo.githubusercontent.com/3ad393a84d74b8695d4ad7ab546fb87d753534b91ea5830d1c85e9198a4d1e82/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f70756e746f6465762f626f6f6b61626c65732e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

A small, **framework-agnostic** PHP library for computing **bookable availability and time slots**. You describe when something is available — with a recurring weekly schedule or a single date range — and Bookables turns that into concrete time ranges and ready-to-book slots for any window of dates.

It's a pure domain library: no framework coupling, no database, no HTTP. Drop it into any PHP application (Laravel, Symfony, plain PHP, …) that needs to answer *"when can this resource be booked?"*. It builds on [`nesbot/carbon`](https://carbon.nesbot.com/)for date/time math and [`league/period`](https://period.thephpleague.com/) for the `Period` value object used to represent ranges and slots.

Requirements
------------

[](#requirements)

- PHP `>=8.4  24,
    'disable_all' => false,
    'daily' => [
        'Mon' => [
            ['start' => '08:00', 'end' => '12:00'],
            ['start' => '14:00', 'end' => '18:00'],
        ],
        'Tue' => [['start' => '09:00', 'end' => '17:00']],
        'Wed' => [],
        // ...Thu, Fri, Sat, Sun
    ],
]);
```

The JSON form (handy for persisting a schedule in a database column):

```
$schedule = WeeklySchedule::fromJson(
    '{"hours_in_advance": 24, "disable_all": false, "daily": {"Sun":[{"start":"14:00","end":"15:00"}]}}'
);

$schedule->toJson();   // serialize back to a JSON string
$schedule->toArray();  // or to an array
```

There's also a ready-made sample schedule (Mon–Fri 08:00–12:00 &amp; 14:00–18:00, Sat 10:00–12:00):

```
$schedule = WeeklySchedule::fromArray(WeeklySchedule::defaultWorkingHours());
```

#### Schedule JSON schema

[](#schedule-json-schema)

KeyTypeDescription`daily`objectMap of day-of-week (`Sun`, `Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`) to a list of `{ "start": "HH:MM", "end": "HH:MM" }` ranges. Times must be a zero-padded time of day (`HH:MM` or `HH:MM:SS`, `00:00`–`23:59`); relative expressions like `now` are rejected. `start` must be before `end`.`hours_in_advance`intMinimum booking notice, in hours. **Metadata only** — stored and exposed via `hoursInAdvance()`, but not enforced by the slotters (see notes below).`disable_all`boolWhen `true`, the schedule yields no availability regardless of `daily`. Optional, defaults to `false`.### 2. Get available ranges from an agenda

[](#2-get-available-ranges-from-an-agenda)

An `Agenda` turns availability into the concrete date ranges that fall inside a requested `[from, to]` window.

```
use Carbon\CarbonImmutable;
use Puntodev\Bookables\Agenda\WeeklyScheduleAgenda;

$agenda = new WeeklyScheduleAgenda($schedule);

$ranges = $agenda->possibleRanges(
    CarbonImmutable::parse('2020-01-20'),
    CarbonImmutable::parse('2020-01-21'),
);

foreach ($ranges as $range) {
    echo $range->toIso8601(), PHP_EOL;
    // 2020-01-20T08:00:00.000000Z/2020-01-20T12:00:00.000000Z
    // 2020-01-20T14:00:00.000000Z/2020-01-20T18:00:00.000000Z
    // ...
}
```

For one-off availability that isn't weekly (e.g. a single open window), use `SingleDateRangeAgenda`. It returns the intersection of its fixed range with the requested window (or no range at all if they don't overlap):

```
use Puntodev\Bookables\Agenda\SingleDateRangeAgenda;

$agenda = new SingleDateRangeAgenda(
    CarbonImmutable::parse('2020-01-20 09:00'),
    CarbonImmutable::parse('2020-01-20 17:00'),
);
```

### 3. Turn ranges into bookable slots

[](#3-turn-ranges-into-bookable-slots)

A `TimeSlotter` slices ranges into the actual slots a user can book.

**`AgendaSlotter`** produces fixed-duration slots inside each of an agenda's ranges:

```
use Puntodev\Bookables\Slots\AgendaSlotter;

// 30-minute slots, back to back
$slotter = new AgendaSlotter($agenda, duration: 30);

$slots = $slotter->makeSlotsForDates(
    CarbonImmutable::parse('2020-01-23'),
    CarbonImmutable::parse('2020-01-23'),
);

// 2020-01-23T08:00:00.000000Z/2020-01-23T08:30:00.000000Z
// 2020-01-23T08:30:00.000000Z/2020-01-23T09:00:00.000000Z
// ...
```

You can reserve a gap before and/or after each appointment (in minutes). The stride between slot starts becomes `duration + max(timeAfter, timeBefore)`:

```
// 50-minute appointments with a 10-minute gap after each → slots every 60 minutes
$slotter = new AgendaSlotter($agenda, duration: 50, timeAfter: 10);

// 2020-01-23T08:00:00.000000Z/2020-01-23T08:50:00.000000Z
// 2020-01-23T09:00:00.000000Z/2020-01-23T09:50:00.000000Z
// ...
```

**`DaySlotter`** ignores agendas entirely and lays a sliding window of slots across the full 24 hours of each day — useful when availability is "any time" and you only care about duration and stepping. When `step` is smaller than `duration`, slots overlap.

```
use Puntodev\Bookables\Slots\DaySlotter;

// 30-minute slots starting every 15 minutes
$slotter = new DaySlotter(duration: 30, step: 15);

$slots = $slotter->makeSlotsForDates(
    CarbonImmutable::parse('2020-01-23'),
    CarbonImmutable::parse('2020-01-23'),
);

// 2020-01-23T00:00:00.000000Z/2020-01-23T00:30:00.000000Z
// 2020-01-23T00:15:00.000000Z/2020-01-23T00:45:00.000000Z
// 2020-01-23T00:30:00.000000Z/2020-01-23T01:00:00.000000Z
// ...
```

### Timezones

[](#timezones)

Agendas compute availability in the timezone of the `Carbon` instances you pass in. `WeeklyScheduleAgenda` interprets the schedule's `HH:MM` times in that timezone. Note that `Period::toIso8601()` renders in UTC (`Z`), so the same wall-clock schedule in different timezones produces different UTC output:

```
$tz = 'Pacific/Auckland';
$ranges = $agenda->possibleRanges(
    CarbonImmutable::parse('2020-01-20', $tz),
    CarbonImmutable::parse('2020-01-20', $tz),
);
// 08:00–12:00 Auckland time → 2020-01-19T19:00:00Z / 2020-01-19T23:00:00Z
```

### Modeling your own bookable entities

[](#modeling-your-own-bookable-entities)

The `HasAgenda` and `TimeBookable` contracts are there for your application to implement on its own models — for example, a professional or room that exposes an agenda:

```
use Puntodev\Bookables\Contracts\Agenda;
use Puntodev\Bookables\Contracts\HasAgenda;

class Professional implements HasAgenda
{
    public function agenda(): Agenda
    {
        return new WeeklyScheduleAgenda($this->weeklySchedule());
    }
}
```

Notes &amp; caveats
-------------------

[](#notes--caveats)

- **`hours_in_advance` is not enforced** by the slotters. It's carried as metadata (available via `hoursInAdvance()`); filtering out slots that are too soon is the consuming application's responsibility.
- **`disable_all` is enforced** — a `WeeklyScheduleAgenda` over a disabled schedule yields no ranges.
- Ranges and slots are immutable `League\Period\Period` objects; all internal date math uses Carbon's immutable variants.
- **Requested date ranges are capped.** `WeeklyScheduleAgenda`, `AgendaSlotter` and `DaySlotter` generate one entry per day (and per slot) in the `[from, to]` window, so an unbounded range would exhaust memory. Each takes an optional `maxDays`argument (default `366`) and throws `Puntodev\Bookables\Exceptions\DateRangeTooLargeException` when the window is larger. Pass `0` (or less) to disable the limit if you have your own bound:

    ```
    use Puntodev\Bookables\Exceptions\DateRangeTooLargeException;

    $agenda  = new WeeklyScheduleAgenda($schedule, maxDays: 92);
    $slotter = new AgendaSlotter($agenda, duration: 30, maxDays: 92);
    $slotter = new DaySlotter(duration: 30, step: 15, maxDays: 92);

    try {
        $slots = $slotter->makeSlotsForDates($from, $to);
    } catch (DateRangeTooLargeException $e) {
        // reject the request (e.g. HTTP 422)
    }
    ```
- **Slot durations must be positive.** `AgendaSlotter` (`duration`) and `DaySlotter`(`duration`, `step`) reject non-positive values with `InvalidArgumentException`; `timeAfter`/`timeBefore` must not be negative.

Testing
-------

[](#testing)

```
composer test
```

Generate an HTML coverage report:

```
composer test-coverage
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for what has changed recently.

Releasing
---------

[](#releasing)

Releases are cut from GitHub and the changelog is kept in sync automatically:

1. Merge the pull requests you want to ship into `master`. Label them so the notes group nicely (`security`, `enhancement`, `bug`, `dependencies`, `documentation`); grouping is configured in [`.github/release.yml`](.github/release.yml).
2. On GitHub, go to **Releases → Draft a new release**, create a `vX.Y.Z` tag following [SemVer](https://semver.org/), and click **Generate release notes**.
3. **Publish** the release. Packagist picks up the new tag, and the [`update-changelog.yml`](.github/workflows/update-changelog.yml) workflow writes the release notes into `CHANGELOG.md` and commits them back to `master`.

The `Unreleased` section in the changelog is just an anchor — release notes flow from the published GitHub release, so there is no changelog to edit by hand.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details. In short: keep the library framework-agnostic, write everything in English, and include tests with every change.

Security
--------

[](#security)

If you discover any security-related issues, please email  instead of using the issue tracker.

Credits
-------

[](#credits)

- [Mariano Goldman](https://github.com/puntodev)

License
-------

[](#license)

The MIT License (MIT). Please see the [License File](LICENSE.md) for more information.

###  Health Score

57

—

FairBetter than 98% of packages

Maintenance97

Actively maintained with recent releases

Popularity22

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity82

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~106 days

Recently: every ~58 days

Total

21

Last Release

4d ago

Major Versions

v0.0.4 → v1.0.02020-11-27

v1.0.0 → v2.0.02021-03-14

v2.6.0 → v3.0.02022-12-14

v3.1.0 → v4.0.02025-02-05

v4.1.2 → v5.0.02026-06-13

PHP version history (7 changes)v0.0.1PHP ^7.4

v1.0.0PHP ^7.4|^8.0

v2.1.0PHP ^8.0

v3.0.0PHP ^8.2

v4.0.0PHP ^8.3

v4.1.0PHP &gt;=8.3 &lt;9.0

v4.1.2PHP &gt;=8.4 &lt;9.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/7201db0e06c12ae2e12e1cf4ee5806b5b465dc538bee3cab6bfb1c0ec52e4dce?d=identicon)[Mariano Goldman](/maintainers/Mariano%20Goldman)

---

Top Contributors

[![marianogoldman](https://avatars.githubusercontent.com/u/959563?v=4)](https://github.com/marianogoldman "marianogoldman (32 commits)")

---

Tags

puntodevbookables

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/puntodev-bookables/health.svg)

```
[![Health](https://phpackages.com/badges/puntodev-bookables/health.svg)](https://phpackages.com/packages/puntodev-bookables)
```

###  Alternatives

[illuminate/support

The Illuminate Support package.

630113.0M41.3k](/packages/illuminate-support)[spatie/holidays

Calculate public holidays

402860.1k2](/packages/spatie-holidays)[craftcms/feed-me

Import content from XML, RSS, CSV or JSON feeds into entries, categories, Craft Commerce products, and more.

293952.6k33](/packages/craftcms-feed-me)[solspace/craft-freeform

The most flexible and user-friendly form building plugin!

54681.3k18](/packages/solspace-craft-freeform)[pimcore/data-importer

Adds a comprehensive import functionality to Pimcore Datahub

46855.5k5](/packages/pimcore-data-importer)[flarum/core

Delightfully simple forum software.

201.4M2.3k](/packages/flarum-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
