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

Feature Toggle Bundle Laravel Package

hotflo/feature-toggle-bundle

hotflo/feature-toggle-bundle adds a simple feature-flag layer to PHP applications, letting teams enable or disable features at runtime without redeploying. Define toggles, check them in code, and roll out changes gradually with less risk.

View on GitHub
Deep Wiki
Context7

Getting Started

First, a reality check that shapes everything below. The description text mentions "Laravel applications," "Blade directives," and "middleware," but the repository is a Symfony2 bundle wrapping JoshuaEstes/FeatureToggle. The only thing the bundle contributes is Symfony DI wiring (Configuration, *Extension, services.yml), a Bundle subclass, and a Twig extension. There is no service provider, no illuminate/* dependency, and no Blade compiler.

Verify before touching anything:

composer show hotflo/feature-toggle-bundle --all

Expect symfony/* pinned in the 2.x range and a PHP constraint from the PHP 5.x era. On a modern Laravel app (PHP 8.2+, Laravel 10/11/12) this will either fail resolution outright or pull obsolete Symfony components into vendor/. Do not composer require it into the application. Treat the repo as reference source for a pattern, not a runtime dependency.

Where to look first (reading it as reference). It is small — likely a few hundred lines. Read in this order:

  1. DependencyInjection/Configuration.php — the config tree; this is the canonical shape of a "toggle" as data.
  2. DependencyInjection/*Extension.php — which services are registered under which IDs/aliases.
  3. Resources/config/services.yml — how the underlying FeatureToggle toggles are wired.
  4. Twig/ extension — the feature_enabled('...') template helper.
  5. vendor/joshuasestes/feature-toggle interfaces (FeatureManager, toggle/strategy contracts) — the only part genuinely worth borrowing.

First use case, done properly. The day-one job this package would serve is: dark-ship a new checkout flow, then expose it to a cohort without redeploying. In Laravel you get the same day-to-day ergonomics from Pennant (first-party, Laravel 10.37+) or ~100 lines of your own:

// app/Features/NewCheckout.php
use Laravel\Pennant\Contracts\FeatureScopeable;

class NewCheckout
{
    public function resolve(User $user): bool
    {
        return $user->isBetaTester()
            || crc32($user->id) % 100 < 10; // sticky 10% rollout
    }
}
// app/Providers/FeatureServiceProvider.php
public function boot(): void
{
    Feature::define('new-checkout', NewCheckout::class);
    Feature::define('new-search', fn (User $u) => in_array($u->plan, ['pro', 'enterprise']));
}

At call sites — controller, job, view-model:

Feature::active('new-checkout') ? new NewCheckoutFlow() : new LegacyFlow();
@feature('new-checkout')
    <x-new-checkout />
@else
    <x-legacy-checkout />
@endfeature

If you specifically want the FeatureManager / named-toggles / pluggable-strategies architecture from JoshuaEstes/FeatureToggle verbatim, expose it behind your own ToggleResolver interface and bind it in a service provider. Do not port the bundle.

Implementation Patterns

Do not store flags in config/*.php if you need runtime flips. config:cache bakes the values into the compiled config, so "enable without redeploy" becomes "enable by redeploying a config cache." Use Pennant's store (database or cache), a features table, or env — not config() calls inside hot paths.

Resolve once per request, not per check. The naive shape hits the store on every Feature::active() call. Pennant's scoped stores handle this; a hand-rolled resolver must read the flag set once and cache it for the request, or you get a query per guarded branch. In hot paths, batch: pull all active flags at the start of the request and check against the array.

Route guarding. Pennant ships EnsureFeaturesAreActive (aliased features); register it in bootstrap/app.php / HTTP kernel and use:

Route::get('/checkout/v2', CheckoutController::class)
    ->middleware('features:new-checkout');

Keep middleware order in mind: feature middleware should run after auth, because most flags are user-scoped and need an authenticated user to resolve.

Service-layer checks, not scattered ifs. Inject the resolver/manager into services and call sites, and prefer a single named boundary:

public function __construct(private ToggleResolver $toggles) {}

public function handle(): Response
{
    return $this->toggles->isActive('new-checkout', $this->user)
        ? $this->newFlow()
        : $this->legacyFlow();
}

This is the ToggleResolver interface recommendation from the technical assessment: call sites depend on the contract, never on Pennant or a fork, so the backend can be swapped (or deleted) later.

Pluggable strategies per flag. Keep resolution logic in a class per feature rather than an if in the provider. That is the one design idea from JoshuaEstes/FeatureToggle worth copying — named toggles resolving against a context. In Pennant this is the resolve() method on a feature class; in a custom implementation it is a Strategy interface with isActive(string $flag, array $context): bool.

Percentage rollouts must be sticky. Hash a stable identifier (user ID, tenant ID) rather than calling rand(), or the same user will flip in and out between requests and destroy the experiment:

return crc32((string) $user->id) % 100 < $percentage;

Strangler adapter for legacy interop. If there is still Symfony2 code in the estate consuming this bundle, do not port the bundle — write an adapter implementing ToggleResolver that proxies to the legacy Symfony2 service during cutover, alongside a Pennant adapter. Delete the legacy adapter when the last Symfony2 process is retired.

Testing patterns. Define a fake resolver or use Pennant's Feature::fake([...]) in tests. Cover the three cases that bite in production: flag on, flag off, and flag missing/undefined. Reset the store between tests (Feature::flushCache()) — leftover active state leaking across tests is the most common CI flake with toggle systems.

Gotchas and Tips

  • The README/description is misleading. It advertises Laravel features the code does not implement. Trust the code and composer.json, not the marketing copy — a description promising "Blade directives" on a Symfony2 bundle is a red flag for the rest of the package's documentation.
  • Composer conflict is likely a hard blocker, not a nuisance. Laravel 10/11 requires Symfony 6.x/7.x components; this bundle almost certainly pins Symfony 2.x. Resolution will fail before any code runs, and a forced resolution leaves you with an unmaintained dependency tree.
  • PHP 5-era code fails static analysis. Untyped properties, missing return types, and @-suppressed calls will trip PHPStan/Pint/CI quality gates even if composer install succeeds.
  • Default-when-undefined semantics must be explicit. Decide per flag type: a kill switch should default to the safe path (fail closed toward the legacy path), while a release toggle can default off. A typo'd flag name silently evaluating to "off" is how features disappear in production with no error surfaced. Log a warning on unknown flag names.
  • config:cache defeats runtime flips. See above. If the flags must be live, the source of truth cannot be a PHP config file.
  • Cache lag breaks kill switches. Any cached store (Redis, in-process array, per-request memoization) lags flag changes by its TTL. For a kill switch, that TTL is your outage window — keep it short or bypass the cache for kill-switch flags.
  • Toggle debt is the dominant real cost. Flags outliving their feature become permanent branches and untested code paths. Adopt a convention now: every flag has an owner, an issue link, and an expiry date; a "remove flag" PR is part of the feature's definition of done. Nothing in this package or its underlying library provides governance, an audit log, or approval flow — you build that discipline yourself.
  • Evaluation divergence. Different services (or the same service with a different cache/context) resolving the same flag differently is a real risk once you fan out. Standardise on a shared store and the same user/tenant context object.
  • Blade directive collisions and naming. If you build your own @feature-style directive, check it does not collide with Pennant's or a host template's. Use a namespaced directive name in a custom implementation.
  • Extension points worth keeping from the original. The FeatureManager + named toggle + pluggable strategy interfaces are sound and framework-agnostic. That is the reusable 10%; the Symfony2 wiring is the discardable 90%. If you fork, isolate it behind your own interface so a future swap is a bind() change, not a grep across the codebase.
  • No targeting, no audit, no admin UI. Neither this package nor JoshuaEstes/FeatureToggle provides percentage bucketing with mutual exclusion, user-attribute targeting, a "who flipped what when" log, or scheduled rollback. If you need those, the answer is Pennant plus your own governance, or Unleash/LaunchDarkly — not this bundle.
  • Zero stars, zero dependents. No issue triage, no downstream validation, no security patch path when PHP or Laravel advances. Treat it as "vendoring a pattern" with a named owner and a documented exit plan, or skip it entirely.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core