Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Subscription Bundle Laravel Package

terox/subscription-bundle

Terox Subscription Bundle provides subscription and plan management for PHP applications, including configurable plans, lifecycle handling, and integration points for recurring billing and payment workflows. Add paid memberships or SaaS-style access control with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Require it as a normal Composer dependency and register the bundle in config/bundles.php (Symfony 4 Flex) or AppKernel::registerBundles() (Symfony 3.3/3.4):

composer require terox/subscription-bundle
// config/bundles.php
return [
    // ...
    Terox\SubscriptionBundle\TeroxSubscriptionBundle::class => ['all' => true],
];

Where to look first, in this order:

  1. The bundle class Terox\SubscriptionBundle\TeroxSubscriptionBundle — confirms the namespace, expected Symfony version, and whether there is a DependencyInjection/Configuration.php (this determines if you configure anything via YAML or are expected to wire services yourself).
  2. The Doctrine entities shipped with the bundle (typically Product, Subscription, and possibly ProductFeature/ProductRestriction). These are the real "API" — you'll be extending or joining against them. Note: the domain noun is Product, not Plan — see Gotchas below.
  3. The manager service Terox\SubscriptionBundle\Manager\ManagerSubscription (or similarly named). This holds the attach/subscribe/cancel logic and is what you call from controllers/commands.
  4. The repository interface SubscriptionRepositoryInterface — as of 0.2.0 this is the contract you implement if you need custom queries (findByProduct($user, $product), findById($id)). Read it before writing your own repository.
  5. The Resources/config folder — check for services.xml/services.yml and any routing. If there's no Configuration.php, do not expect a terox_subscription: YAML block to do anything.

First use case — create a subscription for the current user and read it back (0.2.0 signatures):

// Controller action
$product = $productRepository->findOneBy(['name' => 'monthly']);
$manager->create($product, $this->getUser()); // user is now required as of 0.2.0

// Later, in a service
if ($manager->isSubscribed($user)) {
    // gate a feature
}

Do this in a throwaway /tmp controller before designing anything around the bundle — it tells you whether the manager's method signatures match what the README claims (they often drift in 2018-era bundles, and 0.2.0 has just proven that by reshaping create() and the repository interface).

Given the score and the release history, treat this as a reference implementation, not a dependency. Read the entities, copy the domain model into your app, and skip the composer require. The rest of this assessment assumes you've decided to install it anyway (e.g. an existing legacy app already depends on it, or you're upgrading from 0.1.x).

Implementation Patterns

Entity-centric ownership. The dominant pattern is: your User entity gets a OneToMany to the bundle's Subscription entity. As of 0.2.0, the bundle now models ownership explicitly — ManagerSubscription::create($product, $user) writes the user onto the subscription, and findByProduct(UserInterface $user, ...) scopes lookups by owner. You will almost always need to write an upgrade that:

  • adds the inverse side mapping on your User,
  • exposes getActiveSubscription(): ?Subscription on User,
  • handles the case where the bundle's Subscription references its own Product entity (so your application-level Plan and the bundle's Product are two different things — decide early which one owns which fields).

Manager-as-service. Call into ManagerSubscription from controllers for synchronous actions, and from console commands or Messenger handlers for anything that should be retried (renewals, expiry sweeps, webhook-triggered activation). Typical lifecycle methods to look for: create, cancel, renew, isActive, isExpired, expire.

Explicit owner in every call. Because 0.2.0 made $user mandatory on both create() and findByProduct(), every call site must now supply the owner. This is actually a gift: it forces you to make authorization explicit. Pass the authenticated user down to your service layer rather than relying on a global Security call inside the bundle — the test surface becomes much smaller.

final class Subscriptions
{
    public function __construct(
        private ManagerSubscription $manager,
        private SubscriptionRepositoryInterface $repo,
    ) {}

    public function subscribe(User $user, Product $product): Subscription
    {
        return $this->manager->create($product, $user);
    }

    public function forUser(User $user): iterable
    {
        return $this->repo->findByProduct($user, /* product filter */ null);
    }
}

Product vs. Subscription separation. Keep Product as a static catalogue (name, price, duration, feature flags) and Subscription as the mutable, per-user record (product reference, owner, start/end dates, status, payment reference). This makes the "cancel" path trivial — you never mutate Product.

Gating features. The cleanest integration is to wrap the manager in your own small service and expose intent-revealing methods (canCreateProject(), canInviteUsers()) rather than leaking Subscription objects into controllers/templates. This also means if the bundle dies (it will), you swap the internals behind the same facade.

final class Entitlements
{
    public function __construct(private SubscriptionRepositoryInterface $repo) {}

    public function canInvite(User $user): bool
    {
        return $this->repo->findByProduct($user, /* multi_user product */ null)
            ->exists(fn ($sub) => $sub->isActive());
    }
}

Renewals/expiry via a scheduled command. The bundle will not do this for you. Wire a crontab-driven Symfony command (app:subscriptions:expire) that finds Subscription records past endsAt and calls the manager. Keep the query in your own repository so you control indexes (endsAt, status, and — new in 0.2.0 — user_id).

Templating. If the bundle ships Resources/views (e.g. a product list), extend your base layout by overriding the blocks in templates/bundles/TeroxSubscriptionBundle/. Do not edit the vendor template — Symfony's bundle override path is the intended extension point.

Events. If the bundle dispatches lifecycle events (subscribe/cancel), listen rather than re-implementing side effects. Register listeners in services.yaml for anything transactional (e.g. provisioning a tenant on subscription.created). Since 0.2.0 is a "clean and stabilize" release, check for new events — the changelog doesn't mention any, but the refactor may have introduced them.

Gotchas and Tips

0.2.0 is a breaking release. Three signatures changed. Migrate in this order and you'll avoid half-broken intermediate states:

  1. ManagerSubscription::create($product, $user) — previously ($product) only. Every call site must pass a UserInterface. Before you start, grep for ->create( on the manager and count call sites. If you have a subscribe($user, $plan) wrapper in your app, the change is localized to one line there. If you call the manager directly from controllers, budget for the sweep.
  2. SubscriptionRepositoryInterface::findByProduct now takes a UserInterface parameter (presumably first, following the findByProduct($user, $product) shape). Any custom implementation of the interface must be updated — this includes your own repositories and any mocks in tests. Mock updates are the sneaky part: ->findByProduct(...) in a test expectation will silently return null rather than fail if you don't tighten the argument matcher.
  3. SubscriptionRepositoryInterface::findById is new. If you maintain your own implementation of the interface (not just a Doctrine subclass), you must add this method or the class will be abstract-fatal on load. Add it even if you don't call it.

Upgrade checklist: composer require terox/subscription-bundle:^0.2, then grep -rn "->create(" app/, grep -rn "findByProduct" app/ src/ tests/, then bin/console cache:clear and run your test suite. Do not merge to production without a green test run — the interface changes will surface as Fatal error at class load time, not at call time.

Manager class name is ManagerSubscription, not SubscriptionManager. Easy to get wrong when copy-pasting from generic examples. Use bin/console debug:container | grep -i terox to confirm the actual service ID; it may use a FQCN alias in Symfony 4.1+.

The domain noun is Product, not Plan. The 0.2.0 release notes say create($product, $user). If you've been reading Plan/Subscription tutorials and trying to map them onto this bundle, translate Plan → Product everywhere. Your app can still call its own entity Plan; just don't pass it to create().

Compatibility is still the headline risk. Last release was March 2018; 0.2.0 is a stabilization pass, not a modernization. It still targets Symfony 3.3–4.x APIs (getMockBuilder, @Route annotations maybe, no Symfony 5+ typed service config) and almost certainly has no Symfony 6/7 support. Before installing, check the tag against your symfony/framework-bundle and PHP version. On PHP 8+ you'll likely hit #[\ReturnTypeWillChange]-style warnings or hard errors. Fork and patch if you must.

No configuration extension = no YAML. A common trap is inventing a terox_subscription: config block based on the README. If there's no DependencyInjection/Configuration.php, the extension silently ignores it. Confirm the extension exists before writing config.

Auto-mapping collisions. The bundle's entities may be under its own namespace and mapped to its own table prefix. Now that subscriptions store an owner (user_id), verify the foreign key is added on install/upgrade — doctrine:schema:update --dump-sql should show it. If it doesn't, the bundle's mapping is missing the relationship and you'll need to define it on your side. Joining across the vendor/user boundary is where most debugging time goes.

Doctrine migrations. doctrine:schema:update --dump-sql (or a generated migration) will create/alter its tables — verify the generated schema against the entity annotations before applying in production. Upgrading 0.1.x → 0.2.0 requires a migration that adds user_id to the subscriptions table and backfills it; the bundle will not generate that backfill for you. If the column is NOT NULL, the migration will fail on a non-empty table until you backfill.

Status strings. Bundles of this vintage often use a status column with string constants rather than an enum. Grep the source for const STATUS_ and use those constants; inventing 'active' when the bundle writes 'Active' will cause silent "not subscribed" bugs.

No payment provider. Despite the domain, this is not a billing integration — it does not talk to Stripe, PayPal, or similar. You own the webhook → create/cancel glue. That glue is the only part to write; keep it thin and idempotent. With 0.2.0's explicit $user parameter, the webhook handler is where you must resolve the paying user from your own store before calling create().

Extension point: subclass the manager. Rather than editing vendor code, subclass ManagerSubscription, register the subclass under the same service ID with a decorates: alias, and add your logic there. This survives composer update (which it will need, given how stale it is). Caution: if you subclass and the parent's create() signature changes again in a future release, your override will drift. Pin the minor version.

Debugging trick. bin/console debug:container | grep -i terox and bin/console debug:container --tag=doctrine.repository_service reveal exactly which services the bundle actually registered — useful when the README describes features that were never implemented. After upgrading to 0.2.0, run this again and compare: if the release refactored services, IDs may have shifted.

Test the upgrade path, not just the fresh install. Because 0.2.0 is breaking, write one integration test that: creates a subscription with 0.2.0 signatures, runs findByProduct($user, ...) and asserts the row is returned for that user only, then calls findById($id) and asserts it round-trips. That single test catches the three signature changes at once.

Long-term advice. With a single maintainer and no dependents, 0.2.0 signals the project is being cleaned up but not necessarily revived — one stabilization release does not undo nine years of drift. Budget for a fork-and-maintain outcome, and pin to 0.2.* so a future 0.3.0 breaking release doesn't surprise you. For new projects, model three entities (Product, Subscription, ProductFeature) yourself — it's roughly a day of work and removes an entire class of compatibility risk.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource