worksome/feature-flags
A Laravel package for managing feature flags with typed flag classes and model-based scopes. Define flags in code, toggle them per user or entity, and roll out features gradually with a clean, testable API.
worksome/feature-flags is a small, class-based feature flag package for Laravel. Flags are PHP classes rather than loose config strings, so they are discoverable, testable, and IDE-navigable.
Install and publish
composer require worksome/feature-flags
php artisan vendor:publish --tag="feature-flags-config"
The published config/feature-flags.php is the first file to open — it holds the flag registry/driver settings. If the publish tag differs in your version, run php artisan vendor:publish and pick the package from the list.
First flag in three steps
make-style generator in the package; otherwise just create the class):namespace App\FeatureFlags;
use Worksome\FeatureFlags\Contracts\FeatureFlag;
class NewCheckout implements FeatureFlag
{
public function isActive(): bool
{
return $this->user?->isBetaTester() ?? false;
}
}
Register it in config/feature-flags.php under the flags array, keyed by the string you will reference at call sites (new-checkout).
Resolve it anywhere in the app:
use Worksome\FeatureFlags\Facades\Feature;
if (Feature::active('new-checkout')) {
// new path
}
Confirm the exact facade alias and contract namespace against vendor/worksome/feature-flags/README.md — they are the two things most likely to shift between majors, and the package is currently on the 5.x line.
First use case: guard one new controller action behind a flag and ship it dark. That validates install, config caching, and your test setup in a single small change.
v5.1.0 note: this release adds Laravel 13 support (laravel/framework: ^13.0). It is a minor, non-breaking bump — no code changes required on Laravel 10–12. If you are moving to Laravel 13, check the package's PHP constraint in composer.json before upgrading, and bump this package in the same PR as the framework so you do not sit on an unsupported combination.
One class per flag, dependencies resolved from the container. Keep flag classes thin and side-effect free. Resolve the current user, request, or tenant from the container rather than constructor-injecting a Request (see Gotchas for Octane).
Compose flags from other flags. For related rollouts, a parent flag calling Feature::active('child-flag') keeps the decision tree in one place instead of duplicating audience logic.
Route middleware for gated areas. Rather than sprinkling if statements through controllers, wrap a group of routes in a small middleware that aborts with 404 when the flag is off. This keeps the disabled surface indistinguishable from a non-existent route.
Blade for UI toggles. Use the package's Blade directive (or a plain @if (Feature::active(...))) around nav entries and buttons. Prefer wrapping the entry point rather than the whole view so the old and new paths stay independently testable.
Tests: pin the flags, do not inherit them. Set flag state explicitly in each test (or in a base TestCase setUp) so a developer's local config cannot change CI outcomes. Test both branches of every flag you touch — the "off" branch is the one that rots silently.
// arrange the flag to a known state before hitting the endpoint
Feature::fake(['new-checkout' => true]);
Staged rollout pattern. Start with an allow-list (staff, then beta users), then percentage by a stable hash of user ID, then everyone, then delete the flag and the dead branch in the same PR that flips it to 100%. Treat "delete the flag" as part of the rollout task, not a follow-up ticket.
Config-driven vs. database-driven. The config driver is fine for deploy-time decisions and is the fastest to reason about. If product needs to flip flags without a deploy, use the database-backed driver if your version ships one — and budget for the cache invalidation work that comes with it.
CI and local parity. Keep flag definitions in version control, not in .env. If a flag needs per-environment values, put the audience in env and the decision logic in the class.
config:cache bites. If flag definitions live in config/feature-flags.php, any closure or non-serialisable value there will break php artisan config:cache in production while working fine locally. Run php artisan config:cache once before deploying a new flag.false (or throws, depending on config) — a typo looks exactly like a disabled feature. Add a test that asserts every registered key resolves, and consider a boot-time check in non-production.Request into flag constructors. Inject Illuminate\Contracts\Container\Container or resolve the user/request lazily inside isActive(); otherwise you capture the first request in a worker and leak audience between users.How can I help you explore Laravel packages today?