lunetics/zodiac-bundle
Lunetics ZodiacBundle is a Symfony bundle that adds zodiac sign support to PHP applications, calculating signs from dates and providing services and template helpers for displaying zodiac information.
Read this first: lunetics/zodiac-bundle is a Symfony2 bundle, not a Laravel package. It ships a Bundle class, a DI extension, YAML/XML service definitions, and a Twig_Extension registering a zodiac filter/method. Laravel has no bundle system and Blade is not Twig, so there is no composer require path that produces a working feature. Your day-to-day leverage from this repo is as a reference implementation: copy the boundary table and the 12-way comparison, drop every line of framework glue.
Where to look, in order:
README.md — shows the public surface (a |zodiac Twig filter plus a zodiac(date) method) and what it returns. Note whether it hands back a slug, an English name, or a localized name; that determines your API shape.Tests/ — the boundary assertions. Carry these over as your own PHPUnit data provider rather than re-deriving them.DependencyInjection/, Resources/config/ — ignore. Zero value outside Symfony.Do not install it. Twig 1 APIs (Twig_Extension, Twig_SimpleFilter) were removed in Twig 2/3, and the composer constraints (symfony/framework-bundle ^2.x, twig/twig ~1.x) will hard-conflict with Laravel 10/11, which pins symfony/* to ^6.4/^7. Confirm in 30 seconds if you're curious:
composer why-not lunetics/zodiac-bundle
First use case: show a zodiac sign on a user profile next to their birthdate. Store the birthdate as a date column, resolve the sign on read, render it in Blade.
Start by defining the enum — this is the "12-row lookup table" the whole package reduces to:
// app/Support/Zodiac/Zodiac.php
namespace App\Support\Zodiac;
enum Zodiac: string
{
case Aries = 'aries';
case Taurus = 'taurus';
case Gemini = 'gemini';
case Cancer = 'cancer';
case Leo = 'leo';
case Virgo = 'virgo';
case Libra = 'libra';
case Scorpio = 'scorpio';
case Sagittarius = 'sagittarius';
case Capricorn = 'capricorn';
case Aquarius = 'aquarius';
case Pisces = 'pisces';
public function label(): string
{
return __('zodiac.' . $this->value);
}
}
Then the resolver, which is the only logic worth porting:
// app/Support/Zodiac/ZodiacResolver.php
namespace App\Support\Zodiac;
use DateTimeInterface;
final class ZodiacResolver
{
/**
* month => [day the new sign begins, that sign, the sign ending the day before]
* Source: boundary table vendored from lunetics/zodiac-bundle (MIT).
*/
private const CUTOFFS = [
1 => [20, Zodiac::Aquarius, Zodiac::Capricorn],
2 => [19, Zodiac::Pisces, Zodiac::Aquarius],
3 => [21, Zodiac::Aries, Zodiac::Pisces],
4 => [20, Zodiac::Taurus, Zodiac::Aries],
5 => [21, Zodiac::Gemini, Zodiac::Taurus],
6 => [21, Zodiac::Cancer, Zodiac::Gemini],
7 => [23, Zodiac::Leo, Zodiac::Cancer],
8 => [23, Zodiac::Virgo, Zodiac::Leo],
9 => [23, Zodiac::Libra, Zodiac::Virgo],
10 => [23, Zodiac::Scorpio, Zodiac::Libra],
11 => [22, Zodiac::Sagittarius, Zodiac::Scorpio],
12 => [22, Zodiac::Capricorn, Zodiac::Sagittarius],
];
public function resolve(DateTimeInterface $date): Zodiac
{
[$startDay, $fromStart, $before] = self::CUTOFFS[(int) $date->format('n')];
return (int) $date->format('j') >= $startDay ? $fromStart : $before;
}
}
Bind it once and you can inject it anywhere:
// app/Providers/ZodiacServiceProvider.php
public function register(): void
{
$this->app->singleton(ZodiacResolver::class);
}
1. Resolve on read; don't persist the display name. The sign is a pure function of a calendar date — O(1), no I/O, no cache needed. Persist the slug only if you need it for querying/filtering ("show me all Leos"); never persist label().
// app/Models/Profile.php
protected function zodiac(): Attribute
{
return Attribute::get(
fn () => $this->birthdate
? app(ZodiacResolver::class)->resolve($this->birthdate)
: null
);
}
2. Mirror the original Twig ergonomics with a Blade component. The package's whole DX is {{ user.birthdate|zodiac }}. The Laravel analogue is a component, not a facade:
{{-- resources/views/components/zodiac.blade.php --}}
@props(['date'])
@php($sign = $date ? app(\App\Support\Zodiac\ZodiacResolver::class)->resolve($date) : null)
@if ($sign)
<span class="zodiac-badge" data-zodiac="{{ $sign->value }}">{{ $sign->label() }}</span>
@endif
<x-zodiac :date="$user->birthdate" />
If your team prefers a directive for parity with the filter syntax, Blade::directive('zodiac', ...) works, but it's harder to test and harder to type-check — prefer the component.
3. Validate at the edge, return a typed error, not a default. The package will happily accept any DateTime you hand it, including now(). Don't let null silently become Capricorn:
$request->validate([
'birthdate' => ['required', 'date', 'before_or_equal:today', 'after:1900-01-01'],
]);
4. Use the same data-provider shape as the package's tests. Boundary dates are the only non-trivial thing here, so test both sides of every cutoff, plus leap day and the year wrap:
public static function boundaries(): array
{
return [
'capricorn last day' => ['1990-01-19', Zodiac::Capricorn],
'aquarius first day' => ['1990-01-20', Zodiac::Aquarius],
'pisces first day' => ['1990-02-19', Zodiac::Pisces],
'leap day' => ['1992-02-29', Zodiac::Pisces],
'aries first day' => ['1990-03-21', Zodiac::Aries],
// ... all 24 cutoffs
'dec 31' => ['1990-12-31', Zodiac::Capricorn],
'jan 1' => ['1990-01-01', Zodiac::Capricorn],
];
}
5. Localize via lang files, not the package. Sign names live in lang/en/zodiac.php, lang/es/zodiac.php, etc. The package gives you English only; treat that as a gap to fill, not a feature.
// lang/es/zodiac.php
return ['aries' => 'Aries', 'taurus' => 'Tauro', /* ... */];
6. Keep the interface, hide the table. Bind ZodiacResolver behind an interface if you anticipate swapping traditions later (tropical vs. sidereal, or a product-mandated boundary table). That's the one extension point genuinely worth copying from the package's DI-friendly design.
composer require lunetics/zodiac-bundle fails on symfony/* and twig/twig conflicts before you reach any code-level issue. Even if it resolved, Twig_Extension fatals on Twig 2+. rcrowe/twigbridge does not rescue a Twig 1 extension class.>= vs > on the cutoff day decides whether a cusp user is Aries or Pisces. Pick one convention, document it in a comment above the table, and assert both sides of every cutoff in tests. A wrong sign on a birthday is the most visible possible defect in this feature.datetime and format it in the app timezone (often UTC), a user born late on the 20th can render as the 21st. Use a date column, or CarbonImmutable::parse($value)->startOfDay($userTz) — never created_at-style UTC timestamps.Carbon::create($year, 2, 29) on a non-leap year — it rolls to March 1 and you'll get Aries. Parse the string; let the DB own validity.format(). format('n') gives 1–12 with no leading zero, 'm' gives '01'–'12'. self::CUTOFFS[$date->format('n')] works only because PHP coerces the numeric string; cast to (int) explicitly to avoid a future strict-types surprise.['key' => ..., 'name' => ...]). Keep the vendor shape out of your controllers and JSON — return the enum and serialize ->value at the boundary.ZodiacResolver with an MIT notice in the docblock (as above) and drop every framework file. That's cheaper than an exception in your dependency policy for a 7-star, zero-dependent, unmaintained bundle.How can I help you explore Laravel packages today?