Optimierung 2.7: Company::scopeSearchByTerm fuer die Admin-Combobox

Die in admin/create und admin/edit identische Inline-Firmensuche
(Volltext-oder-LIKE) inkl. des 2x duplizierten supportsFullTextSearch()-
Treiберchecks in einen Company::scopeSearchByTerm() zentralisiert; beide
Editoren nutzen ihn jetzt (Verhalten unveraendert). Ungenutzte DB-Importe
entfernt. Customer nutzt bereits CustomerCompanyContext (eigene
join-/user-gescopte Query, daher nicht zusammengelegt).

Tests: neuer Scope-Test; volle Suite gruen (799 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-19 10:06:54 +00:00
parent 83d2ee659a
commit 86ba6c1a51
4 changed files with 58 additions and 28 deletions

View file

@ -7,12 +7,14 @@ use App\Enums\Portal;
use App\Models\Concerns\HasUniqueSlug;
use App\Scopes\PortalScope;
use Database\Factories\CompanyFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@ -72,6 +74,35 @@ class Company extends Model
];
}
/**
* Firmensuche für die Admin-Combobox: Volltext (name/email/slug) bei
* unterstützenden Treibern und Suchbegriffen ab 3 Zeichen, sonst LIKE.
* Bei leerem Begriff bleibt die Query unverändert. Single Source of Truth
* für die zuvor in mehreren Editoren duplizierte Such- und Treiberlogik.
*/
public function scopeSearchByTerm(Builder $query, string $term): Builder
{
$term = Portal::stripTrailingAbbreviation($term);
if (! filled($term)) {
return $query;
}
$supportsFullText = mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
return $query->where(function (Builder $query) use ($term, $supportsFullText): void {
if ($supportsFullText) {
$query->whereFullText(['name', 'email', 'slug'], $term);
return;
}
$query->where('name', 'like', '%'.$term.'%')
->orWhere('slug', 'like', '%'.$term.'%');
});
}
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_user_id');

View file

@ -10,7 +10,6 @@ use App\Services\Admin\AdminPerformanceCache;
use App\Services\PressRelease\PressReleaseHtmlSanitizer;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Computed;
@ -170,15 +169,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
$term = Portal::stripTrailingAbbreviation($this->companySearch);
$companies = Company::withoutGlobalScopes()
->when(filled($term), function ($q) use ($term): void {
if ($this->supportsFullTextSearch($term)) {
$q->whereFullText(['name', 'email', 'slug'], $term);
return;
}
$q->where('name', 'like', '%' . $term . '%')->orWhere('slug', 'like', '%' . $term . '%');
})
->searchByTerm($term)
->when(blank($term) && $this->companyId, fn($q) => $q->whereIn('id', [(int) $this->companyId]))
->when(blank($term) && !$this->companyId, fn($q) => $q->whereRaw('0 = 1'))
->orderBy('name')
@ -215,10 +206,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::ActiveCategoryOptions, AdminPerformanceCache::OptionsTtl, fn() => Category::query()->with('translations')->where('is_active', true)->orderBy('id')->get());
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3 && in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
}; ?>
<div class="space-y-8 pr-editor-shell" x-data="{ tagInput: '' }">

View file

@ -14,7 +14,6 @@ use App\Services\PressRelease\PressReleaseHtmlSanitizer;
use App\Services\PressRelease\PressReleaseService;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Computed;
@ -351,15 +350,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
$term = Portal::stripTrailingAbbreviation($this->companySearch);
$companies = Company::withoutGlobalScopes()
->when(filled($term), function ($q) use ($term): void {
if ($this->supportsFullTextSearch($term)) {
$q->whereFullText(['name', 'email', 'slug'], $term);
return;
}
$q->where('name', 'like', '%' . $term . '%')->orWhere('slug', 'like', '%' . $term . '%');
})
->searchByTerm($term)
->when(blank($term) && $this->companyId, fn($q) => $q->whereIn('id', [(int) $this->companyId]))
->when(blank($term) && !$this->companyId, fn($q) => $q->whereRaw('0 = 1'))
->orderBy('name')
@ -401,10 +392,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::ActiveCategoryOptions, AdminPerformanceCache::OptionsTtl, fn() => Category::query()->with('translations')->where('is_active', true)->orderBy('id')->get());
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3 && in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
}; ?>
<div class="space-y-8 pr-editor-shell" x-data="{ tagInput: '' }">

View file

@ -0,0 +1,25 @@
<?php
use App\Models\Company;
test('searchByTerm matches name and slug, ignoring non-matches', function () {
$acme = Company::factory()->create(['name' => 'Acme Industries', 'slug' => 'acme-industries']);
$globex = Company::factory()->create(['name' => 'Globex Corporation', 'slug' => 'globex-corp']);
$results = Company::query()->searchByTerm('Acme')->pluck('id');
expect($results)->toContain($acme->id)
->not->toContain($globex->id);
});
test('searchByTerm returns the query unchanged for a blank term', function () {
Company::factory()->count(3)->create();
expect(Company::query()->searchByTerm('')->count())->toBe(Company::query()->count());
});
test('searchByTerm strips a trailing portal abbreviation from the term', function () {
$company = Company::factory()->create(['name' => 'Musterfirma']);
expect(Company::query()->searchByTerm('Musterfirma (PE)')->pluck('id'))->toContain($company->id);
});