boysfromthefactory/groundhog
Groundhog is a lightweight Laravel/PHP package from boysfromthefactory that simplifies recurring, repeatable work in your app. Drop it in, configure a few options, and keep repetitive logic clean, predictable, and easy to test. No extra dependencies.
groundhog_occurrences table and materialized_until column means no cron, no queue worker, no backfill job, no drift between stored and computed data. That is a real headcount-and-on-call decision, not just a schema change: fewer moving parts, fewer "occurrences are stale" bug reports.json_each, MySQL JSON_TABLE, PostgreSQL json_array_elements). Any roadmap item that assumes SQL Server, MariaDB variants, or a second database target is now constrained; RecurrenceNotSupported is thrown elsewhere. This is a decision input for platform standardization, not a detail.groundhog.max_occurrences_per_series now bounds per-series, per-query generation (counting only in-window occurrences). This is the knob for protecting the app from a pathological series (e.g., a minutely rule) without breaking legitimate far-future reads — worth exposing as a product-level limit with a user-facing error message.Adopt when:
Look elsewhere or plan mitigation when:
Upgrade note: v0.2.0 is breaking. It removes the groundhog_occurrences table, groundhog_recurrences.materialized_until, and the max_materialization_ahead config. Upgrading from 0.1 requires php artisan vendor:publish --tag="groundhog-migrations" then php artisan migrate; the shipped migration is a no-op on fresh installs. Budget this as a deploy-window item with a rollback plan, and re-baseline any performance expectations set during 0.1.
For executives: We can ship recurring-schedule features — including far-future views and instant edits — without building or maintaining a scheduling engine ourselves, and without running a background job to precompute occurrences. The trade is that every calendar read does the expansion work at query time: roughly 5–8 seconds for an extreme case (1,000 recurring series over a full year) and proportionally less for normal windows. That is a caching and UX question we control, not a correctness risk. The release also drops a database table and its sync job, which reduces operational surface area. One constraint to accept: it works on SQLite, MySQL, and PostgreSQL only, so it locks in our database choice.
For engineering: v0.2.0 replaces materialization with per-query expansion. All series of a model type are loaded, each rule expanded for the requested window, and the rows passed to the database as a single JSON parameter using json_each (SQLite), JSON_TABLE (MySQL), or json_array_elements (PostgreSQL); other drivers throw RecurrenceNotSupported. There is no read ceiling and no materialized_until. max_occurrences_per_series now caps per-series, per-query generation using only in-window occurrences — this is our abuse guardrail, and it should fail with a surfaced, user-friendly message. Duration-only edits no longer rebuild anything. Cost is O(series × occurrences in window) on every read, so if a surface needs to be fast, add a response/config cache rather than reintroducing materialization. Upgrading from 0.1 is a two-step migration (publish groundhog-migrations, then migrate) that drops the old table and column; treat it as a breaking deploy and re-run your own benchmarks, since 0.1-era performance assumptions no longer apply.
How can I help you explore Laravel packages today?