EV3-431 Profitcenter - betűkód phase1 addd feature flag with laravel pennant
This commit is contained in:
parent
af6c02ad2e
commit
d6c6f0af1c
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "t2t-dev",
|
||||
"runtimeExecutable": "php",
|
||||
"runtimeArgs": ["artisan", "serve", "--port=8123"],
|
||||
"port": 8123
|
||||
}
|
||||
]
|
||||
}
|
||||
51
app/Filament/Resources/FeatureFlags/FeatureFlagResource.php
Normal file
51
app/Filament/Resources/FeatureFlags/FeatureFlagResource.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FeatureFlags;
|
||||
|
||||
use App\Filament\Resources\FeatureFlags\Pages\CreateFeatureFlag;
|
||||
use App\Filament\Resources\FeatureFlags\Pages\EditFeatureFlag;
|
||||
use App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags;
|
||||
use App\Filament\Resources\FeatureFlags\Schemas\FeatureFlagForm;
|
||||
use App\Filament\Resources\FeatureFlags\Tables\FeatureFlagsTable;
|
||||
use App\Models\FeatureFlag;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class FeatureFlagResource extends Resource
|
||||
{
|
||||
protected static ?string $model = FeatureFlag::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-flag';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Rendszer';
|
||||
|
||||
protected static ?string $navigationLabel = 'Feature flagek';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return FeatureFlagForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return FeatureFlagsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListFeatureFlags::route('/'),
|
||||
'create' => CreateFeatureFlag::route('/create'),
|
||||
'edit' => EditFeatureFlag::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FeatureFlags\Pages;
|
||||
|
||||
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateFeatureFlag extends CreateRecord
|
||||
{
|
||||
protected static string $resource = FeatureFlagResource::class;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FeatureFlags\Pages;
|
||||
|
||||
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditFeatureFlag extends EditRecord
|
||||
{
|
||||
protected static string $resource = FeatureFlagResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FeatureFlags\Pages;
|
||||
|
||||
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListFeatureFlags extends ListRecords
|
||||
{
|
||||
protected static string $resource = FeatureFlagResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FeatureFlags\Schemas;
|
||||
|
||||
use App\Models\Role;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class FeatureFlagForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Alapadatok')
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Kulcs')
|
||||
->helperText('Ezt a kulcsot kell használni a kódban: Feature::active(\'kulcs\')')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255),
|
||||
TextInput::make('label')
|
||||
->label('Megnevezés')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Textarea::make('description')
|
||||
->label('Leírás')
|
||||
->columnSpanFull(),
|
||||
Toggle::make('enabled')
|
||||
->label('Bekapcsolva (rendszer szintű kapcsoló)')
|
||||
->default(true),
|
||||
]),
|
||||
Section::make('Célzás')
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
TagsInput::make('stages')
|
||||
->label('Stage-ek')
|
||||
->helperText('Üresen hagyva minden környezeten aktív. Ismert értékek: DEV, TEST, PROD, d2dtst, e2etst.')
|
||||
->placeholder('pl. TEST'),
|
||||
Select::make('roles')
|
||||
->label('Szerepkörök')
|
||||
->helperText('Üresen hagyva minden szerepkörnek aktív.')
|
||||
->multiple()
|
||||
->options(fn () => Role::pluck('display_name', 'name')->filter()->all()),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FeatureFlags\Tables;
|
||||
|
||||
use App\Models\FeatureFlag;
|
||||
use App\Models\FeatureFlagOverride;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\Radio;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class FeatureFlagsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label('Kulcs')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('label')
|
||||
->label('Megnevezés')
|
||||
->searchable(),
|
||||
ToggleColumn::make('enabled')
|
||||
->label('Bekapcsolva'),
|
||||
TextColumn::make('stages')
|
||||
->label('Stage-ek')
|
||||
->badge()
|
||||
->placeholder('mind'),
|
||||
TextColumn::make('roles')
|
||||
->label('Szerepkörök')
|
||||
->badge()
|
||||
->placeholder('mind'),
|
||||
TextColumn::make('updated_at')
|
||||
->label('Módosítva')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
Action::make('userOverride')
|
||||
->label('Egyéni felülbírálás')
|
||||
->icon('heroicon-o-user')
|
||||
->form([
|
||||
Select::make('user_id')
|
||||
->label('Felhasználó')
|
||||
->searchable()
|
||||
->options(fn () => User::pluck('name', 'id'))
|
||||
->required(),
|
||||
Radio::make('override')
|
||||
->label('Állapot')
|
||||
->options([
|
||||
'inherit' => 'Örökölt (alapértelmezett szabály érvényes)',
|
||||
'on' => 'Bekapcsolva',
|
||||
'off' => 'Kikapcsolva',
|
||||
])
|
||||
->default('inherit')
|
||||
->required(),
|
||||
])
|
||||
->action(function (array $data, FeatureFlag $record) {
|
||||
$user = User::findOrFail($data['user_id']);
|
||||
|
||||
if ($data['override'] === 'inherit') {
|
||||
FeatureFlagOverride::where('feature_flag_id', $record->id)
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
} else {
|
||||
FeatureFlagOverride::updateOrCreate(
|
||||
['feature_flag_id' => $record->id, 'user_id' => $user->id],
|
||||
['active' => $data['override'] === 'on'],
|
||||
);
|
||||
}
|
||||
|
||||
// csak ennek az egy usernek a gyorsítótárazott Pennant-feloldását
|
||||
// érvénytelenítjük, a többi felhasználót nem érinti
|
||||
Feature::for($user)->forget($record->name);
|
||||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
12
app/Models/FeatureFlag.php
Normal file
12
app/Models/FeatureFlag.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class FeatureFlag extends BaseAuditable
|
||||
{
|
||||
protected $casts = [
|
||||
'enabled' => 'boolean',
|
||||
'stages' => 'array',
|
||||
'roles' => 'array',
|
||||
];
|
||||
}
|
||||
22
app/Models/FeatureFlagOverride.php
Normal file
22
app/Models/FeatureFlagOverride.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class FeatureFlagOverride extends BaseAuditable
|
||||
{
|
||||
protected $casts = [
|
||||
'active' => 'boolean',
|
||||
];
|
||||
|
||||
public function featureFlag(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(FeatureFlag::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
23
app/Observers/FeatureFlagObserver.php
Normal file
23
app/Observers/FeatureFlagObserver.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\FeatureFlag;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class FeatureFlagObserver
|
||||
{
|
||||
/**
|
||||
* A Pennant lazy módon, scope-onként gyorsítótárazza a feloldott értékeket -
|
||||
* mentés/törlés után purge-elni kell, hogy a módosítás azonnal érvényesüljön.
|
||||
*/
|
||||
public function saved(FeatureFlag $flag): void
|
||||
{
|
||||
Feature::purge($flag->name);
|
||||
}
|
||||
|
||||
public function deleted(FeatureFlag $flag): void
|
||||
{
|
||||
Feature::purge($flag->name);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\FeatureFlag;
|
||||
use App\Observers\FeatureFlagObserver;
|
||||
use App\Repositories\AddressRepository;
|
||||
use App\Repositories\AddressRepositoryInterface;
|
||||
use App\Repositories\ContactRepository;
|
||||
@ -20,6 +22,8 @@
|
||||
use App\Repositories\ProfitCenterRepositoryInterface;
|
||||
use App\Repositories\SupplierRepository;
|
||||
use App\Repositories\SupplierRepositoryInterface;
|
||||
use App\Services\FeatureFlagRegistrar;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@ -53,6 +57,14 @@ public function boot(): void
|
||||
{
|
||||
URL::forceScheme('https');
|
||||
|
||||
FeatureFlag::observe(FeatureFlagObserver::class);
|
||||
|
||||
// Schema::hasTable védi a friss telepítést / migrate-et megelőző state-et,
|
||||
// amikor a feature_flags tábla még nem létezik.
|
||||
if (Schema::hasTable('feature_flags')) {
|
||||
app(FeatureFlagRegistrar::class)->registerAll();
|
||||
}
|
||||
|
||||
\Illuminate\Database\Schema\Blueprint::macro('userIdFields', function () {
|
||||
$this->unsignedInteger('created_by')->nullable();
|
||||
$this->unsignedInteger('updated_by')->nullable();
|
||||
@ -88,6 +100,10 @@ public function boot(): void
|
||||
\Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.create-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\CreateProfitCenterSupplierSchedule::class);
|
||||
\Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.edit-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\EditProfitCenterSupplierSchedule::class);
|
||||
|
||||
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.list-feature-flags', \App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags::class);
|
||||
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.create-feature-flag', \App\Filament\Resources\FeatureFlags\Pages\CreateFeatureFlag::class);
|
||||
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.edit-feature-flag', \App\Filament\Resources\FeatureFlags\Pages\EditFeatureFlag::class);
|
||||
|
||||
\Livewire\Livewire::component('filament.livewire.notifications', \Filament\Livewire\Notifications::class);
|
||||
\Livewire\Livewire::component('filament.livewire.database-notifications', \Filament\Livewire\DatabaseNotifications::class);
|
||||
\Livewire\Livewire::component('filament.livewire.sidebar', \Filament\Livewire\Sidebar::class);
|
||||
|
||||
53
app/Services/FeatureFlagRegistrar.php
Normal file
53
app/Services/FeatureFlagRegistrar.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\FeatureFlag;
|
||||
use App\Models\FeatureFlagOverride;
|
||||
use App\Models\User;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class FeatureFlagRegistrar
|
||||
{
|
||||
/**
|
||||
* Regisztrálja az összes adatbázisban tárolt feature flaget a Pennant-ban.
|
||||
*
|
||||
* Az egyéni felhasználói felülbírálásokat szándékosan NEM a Pennant saját
|
||||
* tárolóján (Feature::for($user)->activate()) keresztül kezeljük, mert a
|
||||
* Pennant purge() minden scope-ra töröl egy adott flag névre - egy admin
|
||||
* mentés (pl. enabled kikapcsolása) így törölné az explicit felülbírálásokat
|
||||
* is. Ezért az override-okat saját táblában tároljuk, a resolver pedig ezt
|
||||
* nézi meg elsőként; a Pennant tárolója így csak a szabály-alapú
|
||||
* alapértelmezett feloldást cache-eli, amit bármikor biztonságos purge-elni.
|
||||
*/
|
||||
public function registerAll(): void
|
||||
{
|
||||
FeatureFlag::all()->each(function (FeatureFlag $flag) {
|
||||
Feature::define($flag->name, function (?User $user) use ($flag) {
|
||||
if ($user) {
|
||||
$override = FeatureFlagOverride::where('feature_flag_id', $flag->id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
if ($override) {
|
||||
return $override->active;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $flag->enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($flag->stages && ! in_array(config('app.stage'), $flag->stages, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($flag->roles && (! $user || ! $user->hasRole($flag->roles))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@
|
||||
"laravel-json-api/laravel": "^5.1",
|
||||
"laravel-lang/lang": "^15.0",
|
||||
"laravel/framework": "^12.48",
|
||||
"laravel/pennant": "^1.24",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
|
||||
79
composer.lock
generated
79
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "05f09a61a6722503ee896c57c530bf6b",
|
||||
"content-hash": "e6b0d54f37a81c8c859c977ad5d3527c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "ahmedhakeem/extra",
|
||||
@ -5084,6 +5084,83 @@
|
||||
},
|
||||
"time": "2026-03-26T14:51:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pennant",
|
||||
"version": "v1.24.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pennant.git",
|
||||
"reference": "b99fbc8038eb2c0a2642e9e13077a75e892b61d9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pennant/zipball/b99fbc8038eb2c0a2642e9e13077a75e892b61d9",
|
||||
"reference": "b99fbc8038eb2c0a2642e9e13077a75e892b61d9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/container": "^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/database": "^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/queue": "^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
|
||||
"php": "^8.1",
|
||||
"symfony/console": "^6.0|^7.0|^8.0",
|
||||
"symfony/finder": "^6.0|^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/octane": "^1.4|^2.0",
|
||||
"orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Feature": "Laravel\\Pennant\\Feature"
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Pennant\\PennantServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php",
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Laravel\\Pennant\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "A simple, lightweight library for managing feature flags.",
|
||||
"homepage": "https://github.com/laravel/pennant",
|
||||
"keywords": [
|
||||
"feature",
|
||||
"flags",
|
||||
"laravel",
|
||||
"pennant"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pennant/issues",
|
||||
"source": "https://github.com/laravel/pennant"
|
||||
},
|
||||
"time": "2026-06-28T17:14:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
"version": "v0.3.16",
|
||||
|
||||
44
config/pennant.php
Normal file
44
config/pennant.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Pennant Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you will specify the default store that Pennant should use when
|
||||
| storing and resolving feature flag values. Pennant ships with the
|
||||
| ability to store flag values in an in-memory array or database.
|
||||
|
|
||||
| Supported: "array", "database"
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('PENNANT_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pennant Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure each of the stores that should be available to
|
||||
| Pennant. These stores shall be used to store resolved feature flag
|
||||
| values - you may configure as many as your application requires.
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => null,
|
||||
'table' => 'features',
|
||||
],
|
||||
|
||||
],
|
||||
];
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Laravel\Pennant\Migrations\PennantMigration;
|
||||
|
||||
return new class extends PennantMigration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('features', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('scope');
|
||||
$table->text('value');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'scope']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('features');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('feature_flags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('label');
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->json('stages')->nullable();
|
||||
$table->json('roles')->nullable();
|
||||
|
||||
// Audit mezők a BaseAuditable-hoz
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feature_flags');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('feature_flag_overrides', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('feature_flag_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->boolean('active');
|
||||
|
||||
// Audit mezők a BaseAuditable-hoz
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['feature_flag_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feature_flag_overrides');
|
||||
}
|
||||
};
|
||||
@ -20,6 +20,7 @@
|
||||
['displayName' => 'Munkanaptárak', 'link' => \App\Filament\Resources\WorkCalendars\WorkCalendarResource::getUrl()],
|
||||
]],
|
||||
['name' => 'calendarTest', 'displayName' => 'Naptár Teszt', 'icon' => 'idopontok', 'link' => \App\Filament\Pages\CalendarTest::getUrl(), 'roles' => ['root','developer','admin']],
|
||||
['name' => 'featureFlags', 'displayName' => 'Feature flagek', 'icon' => 'setup', 'link' => \App\Filament\Resources\FeatureFlags\FeatureFlagResource::getUrl(), 'roles' => ['root','developer']],
|
||||
];
|
||||
@endphp
|
||||
|
||||
|
||||
30
tests/Feature/FeatureFlagAdminUiTest.php
Normal file
30
tests/Feature/FeatureFlagAdminUiTest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags;
|
||||
use App\Models\FeatureFlag;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
test('a lista oldal hiba nélkül renderel, ha egy flagnek van szerepköre és stage-e', function () {
|
||||
$adminRole = Role::create(['name' => 'admin', 'display_name' => 'Admin']);
|
||||
$admin = User::factory()->create();
|
||||
$admin->addRole($adminRole);
|
||||
|
||||
FeatureFlag::create([
|
||||
'name' => 'ui-test-flag',
|
||||
'label' => 'UI teszt flag',
|
||||
'enabled' => true,
|
||||
'stages' => ['TEST'],
|
||||
'roles' => ['admin'],
|
||||
]);
|
||||
|
||||
$this->actingAs($admin);
|
||||
|
||||
Livewire::test(ListFeatureFlags::class)
|
||||
->assertSuccessful();
|
||||
});
|
||||
123
tests/Feature/FeatureFlagTest.php
Normal file
123
tests/Feature/FeatureFlagTest.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
use App\Models\FeatureFlag;
|
||||
use App\Models\FeatureFlagOverride;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Services\FeatureFlagRegistrar;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Laravel\Pennant\Feature;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function defineFlag(array $attributes): FeatureFlag
|
||||
{
|
||||
$flag = FeatureFlag::create(array_merge([
|
||||
'name' => 'test-flag',
|
||||
'label' => 'Test flag',
|
||||
'enabled' => true,
|
||||
'stages' => null,
|
||||
'roles' => null,
|
||||
], $attributes));
|
||||
|
||||
app(FeatureFlagRegistrar::class)->registerAll();
|
||||
|
||||
return $flag;
|
||||
}
|
||||
|
||||
test('a kikapcsolt flag mindenkinek inaktív', function () {
|
||||
defineFlag(['enabled' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('a stage-mismatch inaktívvá teszi a flaget', function () {
|
||||
Config::set('app.stage', 'PROD');
|
||||
|
||||
defineFlag(['stages' => ['TEST']]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('a stage-match aktívvá teszi a flaget', function () {
|
||||
Config::set('app.stage', 'TEST');
|
||||
|
||||
defineFlag(['stages' => ['TEST']]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('a role-mismatch inaktívvá teszi a flaget', function () {
|
||||
Role::create(['name' => 'admin', 'display_name' => 'Admin']);
|
||||
|
||||
defineFlag(['roles' => ['admin']]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('a role-match aktívvá teszi a flaget', function () {
|
||||
$role = Role::create(['name' => 'admin', 'display_name' => 'Admin']);
|
||||
|
||||
defineFlag(['roles' => ['admin']]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->addRole($role);
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('a user-szintű explicit felülbírálás felülír mindent', function () {
|
||||
$flag = defineFlag(['enabled' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
FeatureFlagOverride::create([
|
||||
'feature_flag_id' => $flag->id,
|
||||
'user_id' => $user->id,
|
||||
'active' => true,
|
||||
]);
|
||||
Feature::for($user)->forget('test-flag');
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('a flag admin-mentése nem törli az egyéni felülbírálásokat', function () {
|
||||
$flag = defineFlag(['enabled' => true]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
FeatureFlagOverride::create([
|
||||
'feature_flag_id' => $flag->id,
|
||||
'user_id' => $user->id,
|
||||
'active' => false,
|
||||
]);
|
||||
Feature::for($user)->forget('test-flag');
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
|
||||
|
||||
// Admin módosítja a flaget (pl. leírás) - ez kiváltja a FeatureFlagObserver purge-öt
|
||||
$flag->update(['description' => 'módosítva']);
|
||||
app(FeatureFlagRegistrar::class)->registerAll();
|
||||
|
||||
expect(FeatureFlagOverride::where('feature_flag_id', $flag->id)->where('user_id', $user->id)->exists())
|
||||
->toBeTrue();
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('üres stages/roles esetén mindenkinek aktív', function () {
|
||||
defineFlag([]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user