kukulili-labs/feature-toggle-bundle
A Symfony bundle for managing feature toggles in your application. Define flags, check whether features are enabled for users or environments, and roll changes out gradually without redeploying code.
Step 0 — verify the framework before you write a line of code. Despite the "framework-agnostic core with container integration" claim in the description, the package is named FeatureToggleBundle, and "bundle" is a Symfony convention. As a Laravel developer, the first ten minutes should be spent confirming what you actually have:
composer show kukulili-labs/feature-toggle-bundle
# inspect the "type" field: "symfony-bundle" vs "library"
Then read the repo:
composer.json — type, require block (does it pull symfony/framework-bundle?), and PHP constraint.src/ — is there a *Bundle class extending Symfony\Component\HttpKernel\Bundle\Bundle, or a class extending Illuminate\Support\ServiceProvider?README — installation instructions reference bundles.php (Symfony) or config/app.php / package discovery (Laravel).If the answer is "Symfony-only," stop here. The correct Laravel answer is Laravel Pennant (first-party, maintained, zero integration cost). This package does not belong in a Laravel roadmap unless you're willing to write and own an adapter — which changes the build-vs-buy math entirely.
If it turns out to have a Laravel-compatible container integration, the minimal path is:
composer require kukulili-labs/feature-toggle-bundle — pin an exact version and commit hash. There is no upstream patch release to rely on.config/*.yaml/*.php in the package and a php artisan vendor:publish tag in the README). This is the source of truth for declared toggles.isEnabled('new-checkout') from a single, low-traffic call site and log the result. Verify the value matches what you expect before wiring it into domain logic.First real use case: take one existing config('features.new_checkout') or env('NEW_CHECKOUT') check and migrate it to a declared toggle. This gives you a concrete, reversible baseline to compare against, and it makes the "does this remove conditional logic?" question answerable by Friday.
Bind it behind a first-party interface — this is the single highest-leverage decision.
// app/Contracts/FeatureFlags.php
interface FeatureFlags
{
public function isEnabled(string $flag, mixed $context = null): bool;
}
Bind the package (whatever its concrete evaluator is) to this interface in a service provider. Every call site in your app depends on FeatureFlags, not on the vendor. This caps your migration cost at one class if the package is abandoned — and with 3 stars and 0 dependents, plan for that possibility rather than hoping against it. It also lets tests stub toggles without booting the package.
Declare toggles in one place, with metadata. The package's config-driven approach only works if the config file is treated as the toggle registry. Give each toggle a name, description, owner, default value, and strategy — even if the package schema ignores the extra fields, they're the documentation that keeps toggle debt from becoming permanent.
# config/feature_toggles.yaml (verify exact format against the package)
toggles:
new_checkout:
description: "Gradual rollout of the checkout rewrite"
owner: "payments-team"
expires: "2025-09-30"
strategy: percentage
percentage: 5
refunds_kill_switch:
description: "Ops kill switch for the refunds pipeline"
owner: "platform-oncall"
default: false
strategy: boolean
Percentage rollout: verify determinism. Before you ship a 5% rollout to a PM, confirm that bucketing is a stable hash of a user identifier (user ID, tenant ID, session), not mt_rand(). A non-deterministic bucketing function means the same user flips on and off across requests, which presents to stakeholders as a nondeterministic bug in your feature rather than a flagging bug. Write one test that asserts the same context evaluates to the same result across 100 calls.
Context-aware evaluation: pass the smallest reasonable scope. The description advertises evaluation on user, environment, and request. Pass them explicitly rather than relying on ambient globals:
if ($this->flags->isEnabled('new_checkout', ['user' => $user->id, 'env' => app()->environment()])) {
// new path
}
Explicit context makes tests reproducible and keeps the evaluation contract readable six months from now.
Testing pattern. Stub the FeatureFlags contract, then write a pair of tests for every toggle-gated path: one with the flag on, one off. Add a meta-test that asserts every toggle declared in config has an owner and an expiry — this is the cheapest possible defense against toggle sprawl.
Kill switches vs. release toggles. They have different lifecycles and should be treated differently in code review:
Centralize evaluation for views, middleware, and jobs. Raw FeatureToggle::isEnabled(...) scattered across Blade templates, controllers, and queue workers is the failure mode. Route everything through the interface (or a Blade directive / middleware wrapper) so that when you need to add logging, caching, or a fallback, you do it in one place.
Framework mismatch is disqualifier #1. Re-read the first section if you skipped it. A Symfony bundle cannot be installed into Laravel without a bridge; the effort to write and own that bridge probably exceeds the effort to implement a thin in-house flag interface. Confirm before you commit — this is the single most important fact about the package and it is currently unverified.
Treat it as a reference implementation, not infrastructure. 3 stars and 0 dependents means nobody has publicly hit its bugs. Pin an exact version and commit hash, vendor it into your monorepo if possible, and budget an occasional patch review. Assume you are the maintainer of any fork.
Decide fail-open vs. fail-closed per flag, and test it. What happens when the store (cache, DB, remote) is unavailable at evaluation time? Default-on for risky code paths is how "we killed the feature" becomes "we killed the feature for 60% of users, briefly." The default must be an explicit config field, not whatever the package happens to do on a cache miss.
Long-lived workers are a trap. Configuration and cache are often read once at boot. In Laravel Octane, in a queue worker, or in any long-running process, a flip may not reach that worker until it restarts. Verify the propagation model before relying on a kill switch under load — otherwise you have per-instance drift that presents as nondeterministic bugs in unrelated code.
Cache stampede on cold start. If every worker fetches toggle state simultaneously after a cache flush or an outage recovery, you can bring down the store behind a feature flag. Verify whether the package batches, uses a TTL, or reads at boot. If not, add an application-level warmup.
Percentage bucketing must be deterministic and tested. See above. A flag that flaps per request is worse than no flag, because the bug lands on the feature you were trying to derisk.
Toggle sprawl is the real long-term cost. Every toggle is a branch you must retire. Enforce an owner and expiry at creation time, review flags quarterly, and delete the branch the moment the rollout hits 100%. The package does not solve this; it just makes the branching cheap enough that you'll create more of it.
No audit trail, no RBAC, no approval workflow. If a regulator, a compliance team, or a finance owner will ever ask "who flipped this and when," this package is not the answer. That's not a defect — it's a capability the package doesn't advertise. Pair it with a change log, or choose a maintained alternative if governance is in scope.
Performance. Evaluation happens in hot request paths and inside queued jobs. Confirm the read path is cache-backed and that a flip invalidates across all instances. If it's a DB round trip per evaluation, you've added latency to the exact code paths you were trying to make safer.
Debugging. Log the flag name and the evaluated result at debug level whenever a toggle-gated path errors, and include the current flag state in exception context. When a production incident lands on a feature that changed behavior this week, the first question will be "what was the flag state?" — have the answer in the logs, not in a separate lookup.
Extension points. If the description's claim of "framework-agnostic core with container integration" is accurate, there is probably a strategy interface for custom evaluation. Verify it before building around a percentage-only API — a custom strategy is where you'd put per-tenant overrides or a config-file-to-DB migration shim if you outgrow the built-in strategies.
How can I help you explore Laravel packages today?