v2.0.0
Changelog
v2.0.0 — 2026-04-23
Breaking
plan_subscriptions.slugis no longer globally unique. It is now unique per subscriber:(subscriber_type, subscriber_id, slug). This unblocks polymorphic use — two different subscriber types (e.g.UserandTeam) can now both hold amainsubscription. Existing consumers must rebuild the unique index. SeeUPGRADING.md.- Dropped unused
prorate_day,prorate_period,prorate_extend_duecolumns from theplanstable. These were never read or written by any code path. SubscriptionCreated,SubscriptionCanceled,SubscriptionRenewed, andSubscriptionPlanChangedno longer implementShouldBroadcast.broadcastOn()was returning[]so nothing was ever broadcast. Thesubscriptions.broadcast_eventsconfig key has been removed. Consumers who want broadcasting should extend these events in their own application.PlanSubscription::setNewPeriod()signature changed from(Interval|string $invoiceInterval = '', int $invoicePeriod = 0, Carbon|string $start = '')to(?Interval $invoiceInterval = null, ?int $invoicePeriod = null, ?Carbon $start = null). Only relevant if you were subclassingPlanSubscription.PlanSubscription::recordFeatureUsage()now throwsCrumbls\Subscriptions\Exceptions\UnknownFeatureExceptioninstead ofIlluminate\Database\Eloquent\ModelNotFoundExceptionwhen called with a feature slug that isn't on the subscription's plan.
Fixed
PlanSubscription::active()no longer contained an unreachable branch. Behavior is unchanged for all documented states.- Feature usage increments are now wrapped in a
DB::transaction()withlockForUpdate(). Two concurrent writers can no longer lose a usage increment. newPlanSubscription()now locks the plan row during subscriber-limit enforcement. Two concurrent subscribers to a plan withactive_subscribers_limit = Ncan no longer both squeeze past the check.PlanSubscriptionUsage::scopeByFeatureSlugnow filters via a single subquery instead of fetching the feature row first.subscriptions:prunenow prunes naturally-expired subscriptions too (previously only canceled + expired were pruned), matching the command description.
Added
- Composite index on
(subscriber_type, subscriber_id, slug)and plain index onends_atin theplan_subscriptionstable. Feature::scopeBySlug()andFeature::hasReset()helpers.HasPlanSubscriptions::subscribe()— friendly alias fornewPlanSubscription().- Model factories (
PlanFactory,FeatureFactory,PlanSubscriptionFactory,PlanSubscriptionUsageFactory) with useful state methods (free(),paid(),withTrial(),withGrace(),limitedTo(),ended(),canceled(),onTrial(),resettableMonthly(),resettableDaily()). - GitHub Actions workflows for tests (PHP 8.3/8.4 × Laravel 11/12/13) and static analysis (Pint, PHPStan, Rector).
- Dependabot config, Pint config,
CONTRIBUTING.md,SECURITY.md, issue and PR templates.
v1.1.0 — 2026-04-22
- Add Laravel 13 support (
illuminate/*: ^13.0,orchestra/testbench: ^11.0) - Bump minimum PHP to 8.3 (required by Laravel 13)
- Widen spatie constraints:
eloquent-sortable ^4.4 || ^5.0,laravel-sluggable ^3.8,laravel-translatable ^6.13 - Widen dev constraints: Pest 3|4, pest-plugin-laravel 3|4, PHPUnit 11|12
- Bump
driftingly/rector-laravelto^2.3and add Laravel 12/13 Rector sets; switch Rector PHP set to 8.3
v1.0.0 — 2026-02-20
Breaking Changes (from rinvex/laravel-subscriptions)
- Namespace changed from
Rinvex\SubscriptionstoCrumbls\Subscriptions - Config key changed from
rinvex.subscriptionstosubscriptions - Removed
rinvex/laravel-supportdependency entirely - Removed custom artisan commands (
rinvex:migrate,rinvex:publish,rinvex:rollback) — use standardmigrate/vendor:publish - Removed model-level validation (ValidatingTrait) — use form requests instead
Added
Intervalbacked enum (hour,day,week,month,year) withaddToDate()helper- Grace period support —
onGracePeriod()method and active check - Lifecycle events:
SubscriptionCreated,SubscriptionCanceled,SubscriptionRenewed,SubscriptionPlanChanged subscriptions:pruneartisan command for cleaning up expired subscriptions@propertydocblocks on all models for IDE and static analysis support- PHPStan (level 5) + Larastan — clean
- Pest test suite — 48 tests, 105 assertions (SQLite in-memory)
Modernized
- Requires PHP 8.2+ and Laravel 11/12
casts()method instead of$castsproperty (Laravel 11+ convention)- Anonymous class migrations with
$table->id()andforeignId()->constrained() - Constructor promotion, typed properties,
staticreturn types - Uses Spatie packages directly (
HasSlug,HasTranslations,SortableTrait) - Void return types on closures, modern PHPDoc generics
- Rector (Laravel Shift rules) applied for full Laravel 11 compliance