5.2: gemeinsamen Firmen-Sync-Kern als User::syncCompaniesWithRoles() gezogen

Analyse der beiden ~40/160-Zeilen-save()-Methoden (users/create vs edit): sie
teilen nur den User-Attribut-Payload und den byte-identischen Firmen-Sync-Loop
(Bearbeiten hat zusaetzlich Profil/Kontakte/volle Billing-CRUD). Ein voller
UserUpsertService waere Over-Engineering (ueberwiegend Branches) — daher nur
der wirklich gemeinsame Persistenz-Kern:

User::syncCompaniesWithRoles(array $companyIds, array $roles) baut das
Pivot-Payload (fehlende Rolle => 'member') und synct. Beide Formulare nutzen
es; im Bearbeiten kommen die linkedCompanyIds nun direkt aus $validated.

Test: UserSyncCompaniesTest (Rollen-Zuordnung, member-Default, Detach).
29 User-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-02 08:47:23 +00:00
parent 6b1d84fc01
commit 1729e35943
4 changed files with 57 additions and 15 deletions

View file

@ -482,4 +482,23 @@ class User extends Authenticatable implements MustVerifyEmail
return in_array($companyId, $this->accessibleCompanyIds(), true);
}
/**
* Synchronisiert die Firmenzuordnungen samt Pivot-Rolle. Fehlt zu einer
* Firma eine Rolle, greift 'member'. Gemeinsamer Persistenz-Kern des
* Anlegen- und Bearbeiten-Formulars (admin/users).
*
* @param list<int|string> $companyIds
* @param array<int|string, string> $roles companyId => role
*/
public function syncCompaniesWithRoles(array $companyIds, array $roles): void
{
$payload = [];
foreach ($companyIds as $companyId) {
$payload[$companyId] = ['role' => $roles[$companyId] ?? 'member'];
}
$this->companies()->sync($payload);
}
}

View file

@ -127,13 +127,7 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
$user->syncRoles($validated['selectedRoles']);
$companySyncPayload = [];
foreach ($validated['linkedCompanyIds'] as $companyId) {
$companySyncPayload[$companyId] = [
'role' => $validated['companyRoles'][$companyId] ?? 'member',
];
}
$user->companies()->sync($companySyncPayload);
$user->syncCompaniesWithRoles($validated['linkedCompanyIds'], $validated['companyRoles']);
if ($this->billingIsComplete()) {
$user->billingAddress()->create([

View file

@ -447,15 +447,9 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
$user->profile()->delete();
}
$companySyncPayload = [];
foreach ($validated['linkedCompanyIds'] as $companyId) {
$companySyncPayload[$companyId] = [
'role' => $validated['companyRoles'][$companyId] ?? 'member',
];
}
$user->companies()->sync($companySyncPayload);
$user->syncCompaniesWithRoles($validated['linkedCompanyIds'], $validated['companyRoles']);
$linkedCompanyIds = array_keys($companySyncPayload);
$linkedCompanyIds = $validated['linkedCompanyIds'];
$contactIds = collect($validated['contactForms'])
->pluck('id')

View file

@ -0,0 +1,35 @@
<?php
use App\Models\Company;
use App\Models\User;
test('syncCompaniesWithRoles attaches companies with their pivot role', function () {
$user = User::factory()->create();
$a = Company::factory()->create();
$b = Company::factory()->create();
$user->syncCompaniesWithRoles([$a->id, $b->id], [$a->id => 'owner', $b->id => 'responsible']);
expect($user->companies()->pluck('role', 'companies.id')->all())
->toBe([$a->id => 'owner', $b->id => 'responsible']);
});
test('syncCompaniesWithRoles defaults missing roles to member', function () {
$user = User::factory()->create();
$company = Company::factory()->create();
$user->syncCompaniesWithRoles([$company->id], []);
expect($user->companies()->first()->pivot->role)->toBe('member');
});
test('syncCompaniesWithRoles detaches companies no longer listed', function () {
$user = User::factory()->create();
$a = Company::factory()->create();
$b = Company::factory()->create();
$user->syncCompaniesWithRoles([$a->id, $b->id], []);
$user->syncCompaniesWithRoles([$b->id], [$b->id => 'owner']);
expect($user->companies()->pluck('companies.id')->all())->toBe([$b->id]);
});