happyr/birthday-bundle
A Symfony bundle from HappyR that adds birthday support to your application. It offers birthday-related services and simple configuration to help you track and manage user birthdays within Symfony projects.
First, the reality check. happyr/birthday-bundle is a Symfony bundle, not a Laravel package. It ships a Twig extension, a small Birthday value object, a manager that looks up "who has a birthday soon," and a configurable view template. There is no service provider, no facade, no Blade directive, and nothing Laravel's package discovery will find. composer require happyr/birthday-bundle may resolve (or may not, given its 2017-era constraints on symfony/* and twig/twig), but it will do nothing useful on its own.
That leaves two realistic entry paths:
AppKernel.php, import the config, call the Twig function. That is the supported path and takes minutes.Model/Birthday.php (the date math) and the Twig template (the markup). Both are trivially rewritable in plain PHP + Blade, and you avoid dragging a dead Symfony 2/3 dependency tree into a modern app.Where to look first (in the repo, in this order):
Model/Birthday.php — the whole value of the package lives here: isBirthdayToday(), getDaysUntilBirthday(), getAge(), getNextBirthday(). Read this before anything else; it is what you would actually port.Twig/BirthdayExtension.php — the single birthday_widget() function and how it is wired to the template and to the date_format config.DependencyInjection/Configuration.php — the complete config surface (date_format, widget template path). It is small; there are no hidden options.Resources/views/ — the default widget markup you will override immediately.Minimal Symfony setup (only relevant if you are actually on Symfony):
# app/config/config.yml
happyr_birthday:
date_format: 'Y-m-d'
# widget_template: 'AppBundle:Birthday:widget.html.twig'
// app/AppKernel.php
$bundles = [ /* ... */ new HappyR\BirthdayBundle\HappyRBirthdayBundle() ];
Minimal Laravel port (the path most readers of this assessment want):
// app/Casts/... or trait
trait HasBirthday
{
public function scopeBirthdayToday($q)
{
return $q->whereMonth('birthday', now()->month)
->whereDay('birthday', now()->day);
}
}
Then render with a Blade partial. That is the entire "getting started" for the Laravel case — no package, just the concept.
First use case. The canonical one: a "Upcoming birthdays" block on a profile or directory page, showing the next N users with a birthday, optionally with age. Build that first; it exercises every piece of the package's surface (query window, date math, view helper) and nothing more.
The port map. If you are moving the concept into Laravel, translate each piece explicitly:
| Upstream | Laravel equivalent |
|---|---|
BirthdayInterface::getBirthday() |
HasBirthday trait + birthday column cast to immutable_date |
BirthdayManager lookup |
scopeBirthdayToday / scopeBirthdayWithin(int $days) |
Twig BirthdayExtension |
Blade component <x-birthday-widget> or @birthdays directive |
| Bundle DI config | BirthdayServiceProvider with mergeConfigFrom / publishes |
| Twig template | resources/views/components/birthday-widget.blade.php |
Workflow: value-object first, then query, then view. Implement and unit-test Birthday in isolation before touching Eloquent. It should own: "is today my birthday (in the viewer's timezone)?", "days until next birthday", "next birthday date", "age (if birth year is known)". Everything else is orchestration. This mirrors upstream's structure and keeps the date math testable without a database.
Query window pattern. For "upcoming within N days" you have two options, both present in the reference:
daysUntilNextBirthday() per row, sort, slice. Simple, correct, fine up to a few thousand rows, and the one you should start with.whereRaw on a month/day range, or a precomputed birthday_month_day integer column indexed alongside the tenant/team key. Necessary once the candidate set grows. Do not ship MONTH(birthday) = ? AND DAY(birthday) = ? on a large table; it is a full scan per request.Widget composition. Keep the query side and the presentation side separate, exactly as the bundle does:
// service
class BirthdayService
{
public function upcoming(int $days = 30, int $limit = 5): Collection
{
return User::query()
->select(['id', 'name', 'birthday']) // do not hydrate the world
->birthdayWithin($days)
->limit($limit)
->get();
}
}
{{-- component --}}
@foreach ($birthdays as $user)
<li>
{{ $user->name }}
@if ($showAge) — turns {{ $user->birthday->age }} @endif
<small>{{ $user->birthday->daysUntil }} days</small>
</li>
@endforeach
Integration tip: cache the widget, not the rows. Birthdays change once a year. Cache the rendered fragment per user/team, keyed to local midnight, rather than caching a query result. Cache invalidation is then a date boundary, not a data-event problem.
Integration tip: shadow-run before wiring the UI. If you have any existing manual query or spreadsheet of "who's having a birthday," run the port alongside it for a week and diff the output. Timezone and leap-year bugs surface here, not in your unit tests.
Notification integration. The package does not do notifications, and neither should your first version unless the product asks. If it does, the pattern is a scheduled command that runs the same upcoming(0) query per-tenant at the tenant's local morning and dispatches through your existing notification channel — not a new one.
It is not a Laravel package — hard stop. No extra.laravel.providers, no auto-discovery, no Blade, no Eloquent. Any plan that assumes composer require + php artisan is wrong. The only defensible routes are "port the idea" or "we are on Symfony."
PHP/Symfony era mismatch. Last release 2017-11-14 means PHP 7.0/7.1 and Symfony 2.8/3.x, Doctrine ORM ~2.5, Twig 1/2. On PHP 8.2+ or Laravel 10/11 it will not resolve without --ignore-platform-reqs and a fork — at which point you own a dead fork, which is worse than a 300-line rewrite.
Timezone drift. "Today" is ambiguous. If the server is UTC and the user is UTC+10, a birthday at 09:00 local is tomorrow on the server. Anchor every comparison to the viewer's timezone (now($user->timezone)), not config('app.timezone'). This is the single most common bug in birthday widgets and the package does not solve it.
Leap year (Feb 29). Decide once and write a test: does a Feb 29 birthday fall on Feb 28 or Mar 1 in non-leap years? daysUntilNextBirthday() must have a declared behavior; silently returning 365 or -1 is the failure mode.
Privacy is the real requirement. Date of birth is PII. Decide explicitly: name-only, or name + age? Is the birth year stored at all, or only month/day? Does the person have a say in being listed? The package answers none of this, and no dependency choice removes the obligation — your data model and consent story have to be built deliberately.
Index-friendly queries. MONTH()/DAY() predicates defeat indexes. Persist a generated birthday_month_day column, index it with your tenant key, and query ranges against that. Add a query-count assertion in CI so an N+1 regression on the widget is caught in the pipeline, not in production.
N+1 and hydration. The manager returning full entities is the trap — the widget needs id, display name, and birthday only. select() those, always. If you need the user's avatar or team name, eager-load it explicitly.
Cache boundary. A TTL of "1 hour" will spill past midnight and show yesterday's list for part of the morning. Expire the fragment cache at the local-date boundary (or key it to today()->toDateString() per timezone), not on a fixed interval.
Config quirks are minimal. date_format and widget_template are the whole surface. In the port, keep that shape — a config/birthday.php with date_format, show_age, window_days, widget_view — so behavior is changeable without code edits, matching the upstream contract.
Testing seam. Upstream ships no meaningful tests you can borrow. Write your port with Carbon::setTestNow() around the value object — the whole date-math surface becomes deterministic in a handful of cases (today, tomorrow, yesterday, leap day, year boundary, timezone-shifted viewer). This is where the entire quality of the feature lives; do not skip it.
The bottom line for a day-to-day Laravel developer: 0 stars, 0 dependents, no release in ~8 years, wrong framework. Do not add it to composer.json. Read Model/Birthday.php and the Twig template once, port the concept in under a day with tests, and close the tab.
How can I help you explore Laravel packages today?