devexploris/shizuku-feature-flags
Symfony bundle to manage feature flags stored in Doctrine ORM. Toggle features from the database, check flags in PHP via FeatureFlagService or in Twig with the feature() function, and manage flags with CLI commands to list, create, enable, and disable.
Install via Composer (with abstraction layer):
composer require devexploris/shizuku-feature-flags
Note: Requires Symfony components; use a facade to bridge Laravel/Symfony.
Create a Laravel Service Wrapper (app/Services/FeatureFlagService.php):
namespace App\Services;
use Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService as SymfonyFlagService;
use Illuminate\Support\Facades\Cache;
class FeatureFlagService
{
public function __construct(private SymfonyFlagService $symfonyFlags) {}
public function isEnabled(string $flag): bool
{
return $this->symfonyFlags->isEnabled($flag);
}
}
Register the Bundle in Laravel (via config/app.php or a custom bootloader):
// In a service provider's boot method
$this->app->register(\Devexploris\ShizukuFeatureFlags\ShizukuFeatureFlagsBundle::class);
Generate and Run Migrations (adapt Doctrine schema to Eloquent):
php artisan doctrine:migrations:diff # Requires Laravel Doctrine bridge
php artisan migrate
First Use Case: Check a flag in a Laravel controller:
use App\Services\FeatureFlagService;
class MyController extends Controller
{
public function __construct(private FeatureFlagService $flags) {}
public function index()
{
if ($this->flags->isEnabled('new_ui')) {
return view('ui.new');
}
return view('ui.old');
}
}
php artisan shizuku:flag:create --name=payment_v2 --description="New payment processor" --enable
config/flags.php to map flags to environments:
'flags' => [
'dark_mode' => [
'dev' => true,
'staging' => false,
'production' => env('FEATURE_DARK_MODE', false),
],
],
FeatureFlagService to include environment in cache keys:
$cacheKey = "flags:{$flag}:env:{app()->environment()}";
AppServiceProvider:
Blade::directive('feature', function ($flag) {
return "<?php echo app(\\App\\Services\\FeatureFlagService::class)->isEnabled({$flag}) ? 'true' : 'false'; ?>";
});
@if(feature('new_ui'))
<div class="ui-v2">...</div>
@endif
use Debugbar;
Debugbar::info([
'feature_flags' => [
'checked' => $this->flags->getCheckedCount(),
'enabled' => $this->flags->getEnabledCount(),
'locked' => $this->flags->getLockedFlags(),
],
]);
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
Model::observe(Flag::class, function ($flag) {
Cache::forget("flags:{$flag->name}");
});
Cache Configuration: Override the cache pool in Laravel’s config/cache.php:
'default' => env('CACHE_DRIVER', 'redis'),
Then bind it to the Symfony service:
# config/services.yaml (if using Symfony config)
Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService:
arguments:
$cache: '@cache.connector'
Testing: Mock FeatureFlagService in Laravel tests:
$this->mock(FeatureFlagService::class)->shouldReceive('isEnabled')
->with('payment_v2')->andReturn(true);
Multi-Tenancy: Scope flags to tenants via middleware:
public function handle($request, Closure $next)
{
$tenant = auth()->user()->tenant;
app(FeatureFlagService::class)->setTenant($tenant);
return $next($request);
}
Doctrine vs. Eloquent Schema Mismatch:
DateTimeImmutable vs. Laravel’s Carbon. Fix by casting in the facade:
public function getLockedAt(): ?\Carbon\Carbon
{
return $this->symfonyFlag->lockedAt?->toDateTimeString() ?? null;
}
Cache Invalidation Bypass:
Flag::updated(function ($flag) {
Cache::forget("flags:{$flag->name}");
});
Console Command Conflicts:
artisan:
// In AppServiceProvider
Artisan::alias('shizuku:flag:create', 'make:flag');
Twig Dependency:
feature() directive (above) or a helper class:
class FeatureHelper {
public static function check(string $flag): bool
{
return app(FeatureFlagService::class)->isEnabled($flag);
}
}
Blade usage:
@if(FeatureHelper::check('new_ui'))
Locked Flag Cleanup:
if ($flag->isLocked()) {
\Log::warning("Flag '{$flag->name}' is locked and must be cleaned up.");
}
Unknown Flags: Log unknown flags in a middleware:
public function handle($request, Closure $next)
{
$unknownFlags = app(FeatureFlagService::class)->getUnknownFlags();
if (!empty($unknownFlags)) {
\Log::warning("Unknown flags checked: " . implode(', ', $unknownFlags));
}
return $next($request);
}
Cache Debugging: Dump cache keys to identify stale entries:
\Log::debug('Cache keys:', Cache::getStore()->getAllKeys());
Migration Issues: Use Laravel Doctrine bridge tools to inspect schema:
php artisan doctrine:schema:update --dump-sql
Custom Flag Types:
Flag entity to add metadata (e.g., user_segment, percentage):
class Flag extends \Devexploris\ShizukuFeatureFlags\Entity\Flag
{
protected $percentage; // e.g., 10 for 10% rollout
}
FeatureFlagService to support percentage-based checks:
public function isEnabledForUser(string $flag, User $user): bool
{
$flag = $this->getFlag($flag);
if (!$flag || !$flag->percentage) return $flag->isEnabled;
return $flag->isEnabled && (rand(1, 100) <= $flag->percentage);
}
Webhook Notifications:
use Illuminate\Support\Facades\Event;
Event::listen(Flag::class, function ($flag) {
if ($flag->wasChanged('isEnabled')) {
\Log::info("Flag '{$flag->name}' toggled to {$flag->isEnabled}");
// Send webhook to Slack/Teams
}
});
Audit Logging:
spatie/laravel-audit-log):
How can I help you explore Laravel packages today?