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;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\FeatureFlag;
|
||||||
|
use App\Observers\FeatureFlagObserver;
|
||||||
use App\Repositories\AddressRepository;
|
use App\Repositories\AddressRepository;
|
||||||
use App\Repositories\AddressRepositoryInterface;
|
use App\Repositories\AddressRepositoryInterface;
|
||||||
use App\Repositories\ContactRepository;
|
use App\Repositories\ContactRepository;
|
||||||
@ -20,6 +22,8 @@
|
|||||||
use App\Repositories\ProfitCenterRepositoryInterface;
|
use App\Repositories\ProfitCenterRepositoryInterface;
|
||||||
use App\Repositories\SupplierRepository;
|
use App\Repositories\SupplierRepository;
|
||||||
use App\Repositories\SupplierRepositoryInterface;
|
use App\Repositories\SupplierRepositoryInterface;
|
||||||
|
use App\Services\FeatureFlagRegistrar;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@ -53,6 +57,14 @@ public function boot(): void
|
|||||||
{
|
{
|
||||||
URL::forceScheme('https');
|
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 () {
|
\Illuminate\Database\Schema\Blueprint::macro('userIdFields', function () {
|
||||||
$this->unsignedInteger('created_by')->nullable();
|
$this->unsignedInteger('created_by')->nullable();
|
||||||
$this->unsignedInteger('updated_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.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.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.notifications', \Filament\Livewire\Notifications::class);
|
||||||
\Livewire\Livewire::component('filament.livewire.database-notifications', \Filament\Livewire\DatabaseNotifications::class);
|
\Livewire\Livewire::component('filament.livewire.database-notifications', \Filament\Livewire\DatabaseNotifications::class);
|
||||||
\Livewire\Livewire::component('filament.livewire.sidebar', \Filament\Livewire\Sidebar::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-json-api/laravel": "^5.1",
|
||||||
"laravel-lang/lang": "^15.0",
|
"laravel-lang/lang": "^15.0",
|
||||||
"laravel/framework": "^12.48",
|
"laravel/framework": "^12.48",
|
||||||
|
"laravel/pennant": "^1.24",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "05f09a61a6722503ee896c57c530bf6b",
|
"content-hash": "e6b0d54f37a81c8c859c977ad5d3527c",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "ahmedhakeem/extra",
|
"name": "ahmedhakeem/extra",
|
||||||
@ -5084,6 +5084,83 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-03-26T14:51:54+00:00"
|
"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",
|
"name": "laravel/prompts",
|
||||||
"version": "v0.3.16",
|
"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()],
|
['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' => '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
|
@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