puntodev/bookables 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

puntodev/bookables

Composer 安装命令:

composer require puntodev/bookables

包简介

Bookable Library

README 文档

README

Latest Version on Packagist Build Status Total Downloads License

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 for date/time math and league/period for the Period value object used to represent ranges and slots.

Requirements

  • PHP >=8.4 <9.0
  • ext-json

Installation

Install via Composer:

composer require puntodev/bookables

Concepts

The library is organized around a simple pipeline:

WeeklySchedule  ──▶  Agenda  ──▶  TimeSlotter  ──▶  bookable slots
 (availability)     (concrete       (slices ranges     (Period[])
                     date ranges)    into slots)
Piece Responsibility
WeeklySchedule A value object describing recurring weekly availability (per-day time ranges). JSON/array serializable, with validation.
Agenda (contract) possibleRanges(from, to) → the concrete Period ranges available within a date window.
WeeklyScheduleAgenda An Agenda backed by a WeeklySchedule.
SingleDateRangeAgenda An Agenda for one fixed start/end range.
TimeSlotter (contract) makeSlotsForDates(start, end) → the bookable slots (as Periods) within a window.
AgendaSlotter Slices an Agenda's ranges into fixed-duration slots, with optional gaps before/after.
DaySlotter Produces sliding-window slots across the whole day (ignores any agenda).
HasAgenda / TimeBookable (contracts) Interfaces you implement on your own entities (e.g. a professional, room, or resource).

All ranges and slots are returned as League\Period\Period objects. Call ->toIso8601() (or any Period method) to inspect them.

Usage

1. Define a weekly schedule

A WeeklySchedule describes, for each day of the week, the time ranges during which the resource is available. Build one from an array or from JSON — both validate the input and throw an Exception if it's malformed.

use Puntodev\Bookables\WeeklySchedule;

$schedule = WeeklySchedule::fromArray([
    'hours_in_advance' => 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 & 14:00–18:00, Sat 10:00–12:00):

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

Schedule JSON schema

Key Type Description
daily object Map 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:0023:59); relative expressions like now are rejected. start must be before end.
hours_in_advance int Minimum booking notice, in hours. Metadata only — stored and exposed via hoursInAdvance(), but not enforced by the slotters (see notes below).
disable_all bool When true, the schedule yields no availability regardless of daily. Optional, defaults to false.

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

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

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

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 & 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

composer test

Generate an HTML coverage report:

composer test-coverage

Changelog

Please see CHANGELOG for what has changed recently.

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.
  2. On GitHub, go to Releases → Draft a new release, create a vX.Y.Z tag following SemVer, and click Generate release notes.
  3. Publish the release. Packagist picks up the new tag, and the 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

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

Security

If you discover any security-related issues, please email mariano.goldman@puntodev.com.ar instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see the License File for more information.

puntodev/bookables 适用场景与选型建议

puntodev/bookables 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 7.04k 次下载、GitHub Stars 达 0, 最近一次更新时间为 2020 年 09 月 02 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「puntodev」 「bookables」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 puntodev/bookables 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 puntodev/bookables 我们能提供哪些服务?
定制开发 / 二次开发

基于 puntodev/bookables 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 7.04k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 15
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2020-09-02