Začínáme
Autorizace
Každé odmítnutí jde přes laravelí Gate — na co se ho ptá tabulka, formulář, akce a stránka, a kdy.
Na této stránce
Wire používá Laravel Gate a policies. Autorizace tak zůstává kompatibilní s nativními Laravel policies, Spatie Permission a balíčky, které registrují abilities do Gate.
Sdílená pravidla komponent
Sloupce, filtry, akce, pole a widgety mohou používat sdílené autorizační metody, když komponenta podporuje viditelnost nebo autorizaci.
Action::make('approve') ->label('Approve') ->authorize('approve') ->action(fn (Order $record) => $record->approve()); TextColumn::make('internal_note') ->label('Internal note') ->permission('orders.internal-notes.view'); SelectFilter::make('department_id') ->authorizeUsing(fn (User $user) => $user->is_admin);
Pořadí vyhodnocení:
| Pravidlo | Chování |
|---|---|
| Žádná autorizace nakonfigurovaná | Povoleno |
| Žádný přihlášený uživatel | Zamítnuto |
authorizeUsing() |
Vlastní callback má prioritu |
authorize() |
Kontroluje Laravel Gate ability |
permission() |
Kontroluje permission řetězec přes Gate |
Autorizace po záznamech
Callback authorizeUsing() dostává přihlášeného uživatele a — kde ho povrch má — záznam řádku, takže autorizaci lze zúžit na jednotlivé záznamy:
Action::make('approve') ->authorizeUsing(fn (User $user, $record) => $user->id === $record?->manager_id) ->action(fn (Order $record) => $record->approve());
Záznam je přítomný u řádkových akcí; je null u povrchů bez záznamu (strukturální viditelnost sloupce/filtru, pole, widgety), takže jednoargumentová closura fn ($user) => … zůstává platná všude.
Tohle řídí, zda celý sloupec/akce strukturálně existuje (vyhodnoceno jednou). Pro skrytí nebo redakci jedné buňky na řádek — např. zobrazit salary jen na záznamech, které uživatel smí vidět — použijte místo toho visibleForRecord() sloupce, který běží při renderu buňky se záznamem daného řádku:
TextColumn::make('salary') ->visibleForRecord(fn ($record) => auth()->user()->can('viewSalary', $record));
Policies tabulky
Zapněte kontroly policies na tabulce pomocí authorize().
use NyonCode\WireTable\Table; public function table(Table $table): Table{ return $table ->model(Order::class) ->authorize() ->columns([ // ... ]);}
Wire kontroluje tyto policy metody, když jsou potřeba:
| Schopnost tabulky | Policy ability |
|---|---|
| Vytvořit záznam | create |
| Aktualizovat záznam | update |
| Smazat záznam | delete |
| Zobrazit záznam | view |
Přepisy tabulky
Přepisy použijte, když tabulka potřebuje pravidla odlišná od policy modelu.
return $table ->model(Order::class) ->authorize() ->authorizeCreate(fn () => auth()->user()?->can('create', Order::class) ?? false) ->authorizeUpdate(fn (Order $record) => ! $record->is_locked) ->authorizeDelete(fn (Order $record) => $record->status === 'draft') ->authorizeView(fn (Order $record) => $record->tenant_id === auth()->user()?->tenant_id);
Každý přepis přijímá boolean nebo closuru.
Inline editace
Editovatelné sloupce mohou pro inline editaci vyžadovat Gate ability.
TextInputColumn::make('price') ->authorizeInline('orders.update-price');
Pokud uživatel neprojde kontrolou ability, sloupec zůstane viditelný, ale inline editace je zamítnuta.
Akce
Akce lze skrýt nebo zamítnout pomocí Gate abilities, permission řetězců nebo vlastních callbacků.
Action::make('refund') ->label('Refund') ->authorize('refund') ->visible(fn (Order $record) => $record->is_paid) ->requiresConfirmation() ->action(fn (Order $record) => $record->refund());
Pro jednoduché permission řetězce:
Action::make('export') ->permission('orders.export') ->action(fn () => $this->exportTable());
Formuláře
Formuláře mohou používat policies modelu pro create a update.
use NyonCode\WireForms\Forms\Form; public function form(Form $form): Form{ return $form ->model($this->user ?? User::class) ->authorize() ->schema([ // ... ]);}
Když je authorize() zapnuto:
| Stav formuláře | Policy ability |
|---|---|
| Třída modelu nebo neuložený model | create |
| Existující instance modelu | update |
Při zamítnutí je formulář read-only a nelze ho uložit.
Pro vlastní pravidla formuláře:
return $form ->model($this->user) ->authorizeUsing(fn (User $user) => $user->hasRole('editor')) ->schema([ // ... ]);
Sortable
Sortable operace by měly být chráněné v hoocích vaší Livewire komponenty.
public function beforeRowsReordered(array $orderedIds): void{ $this->authorize('reorder', Task::class);}
Hooky životního cyklu viz Sortable řazení řádků.
Související dokumentace
| Dokument | Co pokrývá |
|---|---|
| Core Akce | Řádkové, hromadné, hlavičkové akce a modální akce |
| Přehled tabulek | Nastavení tabulky a API na úrovni tabulky |
| Přehled formulářů | Nastavení formuláře a chování ukládání |
| Audit Log | Záznam změn modelů po úspěšné autorizaci |
Multi-tenancy
Ve výchozím stavu vypnutá — většina aplikací má jednoho tenanta a scopovat je by
byla WHERE klauzule koupená za nic. Jakmile je zapnutá, je striktní.
Multi-tenant aplikace jsou tři kusy a každý má vlastní stránku:
| Kus | Co dělá | Kde |
|---|---|---|
| Tenantová zóna | Dá firmu do URL, ověří proti ní člověka, vstoupí do ní a přenese ji do každého odkazu | Tenancy |
| Data | Omezí každý model vlastněný tenantem na firmu, do které se vstoupilo — řádky, nebo vlastní databáze | tato sekce |
| Firmy | Registrace firmy, její profil, pozvánky a správa členů | Firmy |
Zóna a data stačí; modul firem jsou obrazovky, které by si aplikace jinak psala sama.
// config/wire-core.php'tenancy' => [ 'enabled' => true, 'column' => 'tenant_id', 'isolation' => 'column', // or a class implementing IsolatesTenants],
Aktuální tenant je ten, do kterého se vstoupilo (viz níže), a když se nevstoupilo nikam, žádný tenant není — se zapnutou tenancy to znamená prázdnou stránku. Aplikace bez tenanta v URL — třeba každý člověk v právě jedné firmě — odpoví jinak: naváže vlastní resolver, který výchozí úplně nahradí:
use NyonCode\WireCore\Core\Tenancy\Contracts\TenantResolver; app()->bind(TenantResolver::class, fn () => new class implements TenantResolver { public function resolve(): int|string|null { return auth()->user()?->tenant_id; }});
Pak označ modely, které tenantovi patří:
use NyonCode\WireCore\Core\Tenancy\Concerns\BelongsToTenant; class Invoice extends Model{ use BelongsToTenant; }
Zapíná se pro každý model zvlášť, protože framework nemůže vědět, které z vašich tabulek tenant vlastní, a hádat by znamenalo hádat, kdo co smí vidět.
Vstup do tenanta
CurrentTenant drží tenanta, ve kterém se pracuje, a výchozí resolver ho čte —
takže vstoupit do tenanta stačí k tomu, aby ho následoval každý scopovaný model.
Pro request to udělá middleware tenantové zóny; job, příkaz, seeder nebo test to
udělá přes runAs():
use NyonCode\WireCore\Core\Tenancy\Tenancy; app(Tenancy::class)->runAs($company, function (Company $company) { Invoice::query()->where('due_at', '<', now())->each->remind(); });
runAs() obnoví, co bylo aktuální předtím — jiného tenanta, nebo žádného —, i když
callback vyhodí výjimku, a vnořená volání se rozbalí v pořadí. Držitel je
scoped: request pod Octane i job ve frontě začínají bez tenanta, takže ten,
kdo zapomene vstoupit, neuvidí žádné řádky, a ne řádky předchozího jobu.
Práce ve frontě si nese svého tenanta. Job zařazený uvnitř tenanta si do
payloadu zapíše třídu a klíč tenanta a worker do něj před handle() vstoupí —
takže notifikace, export nebo import spuštěný v Acme poběží v Acme, na workeru,
který o requestu nic neví. Job zařazený mimo tenanta běží mimo něj a podle
fail-safe nevidí žádné řádky. Job, jehož tenant byl mezitím smazán, selže,
místo aby běžel v žádném. Job sync běží uvnitř requestu a vlastní tenant
requestu se mu po doběhnutí vrátí.
Funguje i runAs($company, fn () => SendReport::dispatch()). dispatch() vrací
PendingDispatch, který job zařadí, až zanikne, a vrácený z callbacku by zanikl
až po opuštění tenanta — proto ho runAs() zahodí ještě uvnitř, job se zařadí
v tenantovi a volání vrátí null.
Jak se tenanti od sebe oddělují, říká isolation. column — jedna databáze,
sloupec tenanta — je výchozí a nepotřebuje nic kromě tenanta samotného; třída
implementující IsolatesTenants (enter(Model $tenant), leave()) je způsob, jak
připojit jinou strategii, mezi nimi databázi pro každého tenanta.
Databáze pro každého tenanta
Když smlouva žádá oddělení tenantů něčím víc než klauzulí WHERE, nastavte
isolation na database. Každý tenant má pak vlastní databázi a hranicí je
připojení, ne sloupec:
// config/wire-core.php'tenancy' => [ 'enabled' => true, 'isolation' => 'database', 'model' => App\Models\Company::class, 'database' => [ 'connection' => 'tenant', // definované v config/database.php 'name' => 'tenant_{key}', // {key}, {slug}; u SQLite cesta k souboru 'admin_connection' => null, // kdo spouští CREATE DATABASE 'migrations' => 'database/migrations/tenant', ], ],
Modely tenanta použijí BelongsToTenantDatabase místo BelongsToTenant — bez
sloupce a bez scope; čtou se přes připojení tenant, které vstup do tenanta
nasměruje na jeho databázi. Firmy, uživatelé a kdo kam patří zůstávají na
vlastním připojení aplikace.
php artisan wire:tenants:create acme globex # vytvořit a zmigrovatphp artisan wire:tenants:create --all --no-migratephp artisan wire:tenants:migrate # každý tenantphp artisan wire:tenants:migrate --tenant=acme --fresh --seed
Databáze se vytváří přes schema builder administrativního připojení — CREATE DATABASE na MySQL a PostgreSQL, prázdný soubor na SQLite —, takže uživatel toho
připojení na to musí mít právo. Bez vstupu do tenanta připojení tenant
nejmenuje žádnou databázi a první dotaz vyhodí výjimku: stejný směr jako
prázdný výsledek scope u sloupce, jen hlasitěji. Všechno ostatní — runAs(),
tenantová zóna, joby ve frontě — funguje pod oběma stejně.
Fail-safe
Zapnutá tenancy bez resolvovaného tenanta vrací nic, nikdy vše.
To je celý bezpečnostní příběh v jedné větě. Každý běžný stav vyrobí null
tenanta — před přihlášením, na queue workeru, v konzolovém příkazu — takže scope,
který by null četl jako „bez omezení“, by každému z nich podal všechny řádky.
Místo toho omezí na 0 = 1.
Záměrně to není ani where tenant_id is null: řádek, který nikdo nevlastní, by
pak viděli všichni, což je tentýž únik v jiném kabátě.
Co je scopované
Globální scope, ne plugin hook — a ta volba je pointa. Hook pokryje jednu čtecí cestu, a jen když je zrovna navázaný plugin manager. Globální scope pokryje každý dotaz, který Eloquent postaví:
| Scopované | |
|---|---|
Invoice::query(), výpis tabulky, relace |
ano |
Invoice::find($id) ve vašem vlastním controlleru |
ano |
->update() a ->delete() |
ano |
| Frontovaný job resolvující záznamy podle klíče | ano |
Invoice::create() |
připíše se aktuálnímu tenantovi |
Create bez resolvovaného tenanta vyhodí výjimku místo zápisu řádku s null tenantem — takový řádek by pak byl neviditelný pro každý scopovaný dotaz, takže by uživatelova práce byla pryč a nikdo by to neřekl. Explicitně nastavený sloupec se nechá být, kvůli seederu nebo záměrnému přesunu mezi tenanty.
Sloupec je kvalifikovaný, protože scopovaný model se běžně joinuje a joinovaná
tabulka mívá vlastní tenant_id.
Čtení napříč tenanty
Invoice::acrossAllTenants()->count();
Záměrně upovídané a snadno greppovatelné: admin report nebo konzolový příkaz má reálnou potřebu a revize musí najít každé místo, které si ji nárokovalo.
Zdroj, na který Eloquent nedosáhne
Non-Eloquent DataSource — CollectionDataSource, zdroj nad API — nestaví
žádný Eloquent dotaz, takže na něj globální scope nedosáhne. Obalte ho:
use NyonCode\WireCore\Core\Tenancy\TenantScopedDataSource; $source = new TenantScopedDataSource(new CollectionDataSource($rows), app(Tenancy::class));
Omezuje plán, ne řádky, které se vrátí — a právě proto je bezpečný na všech
metodách, ne jen na té, která řádky vrací: count() a paginate() odpovídá
zdroj, aniž by řádky vůbec vydal, a zdroj, který ten filtr neumí, ho hlasitě
odmítne (UnsupportedQueryAspectException) místo aby vrátil řádky, které nikdo
neomezil.
resolveRecord() bere klíč, ne plán, takže tam se záznam načte a zkontroluje —
bez toho se tenant dostane na cizí řádek tím, že si do URL napíše jeho id.
Fail-safe je tentýž, jaký má TenantScope: tenancy zapnutá a žádný tenant
rozpoznaný znamená nic. Vypnutá tenancy deleguje beze změny, takže obalení
zdroje jednotenantovou aplikaci nic nestojí.
Třetím argumentem předáte sloupec tam, kde si ho zdroj pojmenoval jinak než konfigurace.