presseportale/app/Models/LegalRequest.php
Kevin Adametz e783adc54e technik: Statische Analyse wieder aussagekräftig machen
phpstan lief mit 390 Meldungen außerhalb der Baseline, der größte Teil davon
Falschmeldungen. Ursache: Larastan löste keinen einzigen Cast auf, weil alle
Models ihre Casts über die casts()-METHODE deklarieren (Laravel-11-Stil).
Laravel führt die erst zur Laufzeit mit $casts zusammen, Larastan instanziiert
Models aber ohne Konstruktor — $pressRelease->status galt deshalb als string
und jeder Enum-Vergleich als "immer falsch". Dafür gibt es den Schalter
parseModelCastsMethod, dessen Larastan-Default false ist.

Zweite Sammelursache waren fehlende Generics an den Relations-Methoden: Ohne
sie landete phpstan auf Illuminate\Database\Eloquent\Model und meldete jede
Modell-Eigenschaft und jeden eigenen Scope als unbekannt. Alle 102
Relationen tragen jetzt ihr @return <Related, $this>; das verbessert
nebenbei die Vervollständigung in der IDE.

Zusammen fällt die Zahl von 390 auf 169. Die neu erzeugte Baseline umfasst 166
Einträge, überwiegend Closure-Parameter, die als blankes Builder typisiert
sind, sowie zwei nachgeprüfte Falschmeldungs-Gruppen: die Concerns unter
app/Livewire werden ausschließlich aus Volt-Komponenten eingebunden, die
phpstan nicht analysiert, und Cashier typisiert findBillable() gegen den TRAIT
Billable, was phpstan nicht mit User vereinbaren kann — die Laufzeitprüfung
dort ist richtig und bleibt.

Was die Analyse an echten toten Zweigen zutage gefördert hat, ist mit
entfernt: eine doppelte Null-Prüfung im Nachweis-PDF, Prüfungen gegen
NOT-NULL-Spalten in der Rechnungsstellung und im Vorschau-Controller, eine
immer wahre Bedingung im Logo-Pfad und in RetireAgencyCompanyType, ein
überflüssiges ?? [] auf einer Symfony-Methode, die immer ein Array liefert,
sowie der unerreichbare null-Arm in publicPortalLinks(). MigrateLegacyMedia
deklariert seine Medientypen jetzt als Literal-Union, womit das match ohne
default nachweislich vollständig ist statt nur faktisch.

Damit das nicht wieder abdriftet — die Baseline stand seit Mai unverändert —
läuft die Analyse ab jetzt als eigener Schritt in der Test-Pipeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:07:51 +00:00

130 lines
3.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use App\Enums\LegalRequestStatus;
use App\Enums\LegalRequestType;
use App\Enums\LegalRequestVerificationMethod;
use Database\Factories\LegalRequestFactory;
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\HasMany;
/**
* Rechts-/Compliance-Anfrage (WS-3) – DSGVO, Persönlichkeitsrecht oder Meldung.
* Bewusst OHNE PortalScope: der Admin-Bereich „Recht & Compliance" sieht alle
* Portale; das Portal wird zur Anzeige/Filterung als eigene Spalte gehalten.
*/
class LegalRequest extends Model
{
/** @use HasFactory<LegalRequestFactory> */
use HasFactory;
protected $fillable = [
'press_release_id',
'portal',
'type',
'status',
'requester_email',
'message',
'payload',
'requester_ip',
'verified_at',
'verification_method',
'single_purchase_id',
'resolved_by_user_id',
'admin_note',
'resolved_at',
'executed_at',
];
protected function casts(): array
{
return [
'type' => LegalRequestType::class,
'status' => LegalRequestStatus::class,
'payload' => 'array',
'verified_at' => 'datetime',
'verification_method' => LegalRequestVerificationMethod::class,
'resolved_at' => 'datetime',
'executed_at' => 'datetime',
];
}
/**
* Öffentliche Vorgangsnummer (T10) — analog zur Kündigung ("K-{id}").
* Wird in der Eingangsbestätigung genannt und bei Rückfragen referenziert.
*/
public function reference(): string
{
return 'M-'.$this->id;
}
/**
* Name des Meldenden (Standalone-Formulare, DSA Art. 16) — strukturierte
* Zusatzfelder liegen bewusst im payload (WS-3).
*/
public function requesterName(): ?string
{
return $this->payload['requester_name'] ?? null;
}
/**
* Gemeldete URL, wie vom Meldenden angegeben (Standalone-Formulare).
*/
public function reportedUrl(): ?string
{
return $this->payload['reported_url'] ?? null;
}
/**
* @return BelongsTo<PressRelease, $this>
*/
public function pressRelease(): BelongsTo
{
return $this->belongsTo(PressRelease::class);
}
/**
* @return BelongsTo<User, $this>
*/
public function resolver(): BelongsTo
{
return $this->belongsTo(User::class, 'resolved_by_user_id');
}
/**
* Bezahlvorgang der kostenpflichtigen Spuren (Depublizierung/Änderung).
*
* @return BelongsTo<SinglePurchase, $this>
*/
public function singlePurchase(): BelongsTo
{
return $this->belongsTo(SinglePurchase::class);
}
/**
* Verschlüsselte Vorher-Snapshots ausgeführter Mutationen (Korrektur/
* Anonymisierung) — Grundlage der Rückgängig-Option bis zur Löschfrist.
*
* @return HasMany<PressReleaseRedactionSnapshot, $this>
*/
public function redactionSnapshots(): HasMany
{
return $this->hasMany(PressReleaseRedactionSnapshot::class);
}
/**
* @param Builder<LegalRequest> $query
* @return Builder<LegalRequest>
*/
public function scopeOpen(Builder $query): Builder
{
return $query->whereIn('status', [
LegalRequestStatus::Open->value,
LegalRequestStatus::InProgress->value,
]);
}
}