9 KiB
07 – API-Migration
1. Ausgangslage (Legacy)
- Authentifizierung: Statischer
api_key-String im User-Profil (sfGuardUserProfile.api_key). - Transport via HTTP-Header oder Query-Param, je nach Client (historisch gewachsen).
- Symfony-Module
pressrelease,pressreleaseimage,company,category,newsletterstellten Actions bereit (ApiActions-Basisklasse). - Format: JSON (teils auch XML) – wurde via
renderText(json_encode(...))erzeugt. - CSRF für API-Requests in
prepareForm()abgeschaltet. - Undokumentiert – Schnittstellen-Wissen liegt in Server-Logs und im Symfony-Code verstreut.
Vor der finalen Migration: Zugriffs-Log des Prod-Servers der letzten 3 Monate analysieren, um vollständige Liste der tatsächlich genutzten Endpoints zu erhalten. (Aufgabe zu Beginn Phase 7.)
2. Ziel-Architektur
- URL-Prefix:
/api/v1 - Auth: Nur Laravel Sanctum Personal Access Tokens (Bearer).
- ⭐ Entscheidung D-05 / Q-03: Kein Kompatibilitätsmodus für Legacy-Keys. Cut-over statt Keep-Alive.
- Format: JSON only;
Accept: application/jsonempfohlen. Validation-Fehler als RFC 7807 (Laravel Default). - Versionierung:
v1= neuer Stand, saubere Ressourcen (kein Legacy-Feld-Alias mehr nötig, weil Clients eh umstellen müssen). - Rate-Limiting: Pro Sanctum-/Bearer-Token 60/min.
- Dokumentation: OpenAPI-YAML (
docs/api/v1.yml) + optional Scribe-Renderer.
Token-Scopes (Sanctum Abilities)
| Ability | Zweck |
|---|---|
press-releases:read |
Listen + Details lesen |
press-releases:write |
Erstellen / Update / Delete |
press-release-images:write |
Upload / Delete |
companies:read |
Firmen lesen |
newsletter:subscribe |
Newsletter-Opt-In |
Token können im Customer-Portal (und Admin) mit individuellen Scopes erstellt werden.
3. Cut-Over-Strategie für Legacy-API-Clients
Kein Parallelbetrieb. Kommunikation in 3 Phasen:
| Phase | Zeitpunkt | Aktion |
|---|---|---|
| Ankündigung | T-30 Tage | Mail an alle User mit api_key_legacy != null: neue Technik kommt, Token-Migration nötig, Link zur Doku |
| Erinnerung | T-7 Tage | Zweite Mail |
| Cut-over | Go-Live | Alter api_key wird abgewiesen. Response 410 Gone mit message: "Legacy API keys are no longer supported.", migration_url und docs_url |
Aktive Kunden bekommen im Backend / Customer-Portal eine prominent platzierte Migrations-Anleitung + Token-Generator.
4. Endpoint-Mapping (vorläufig)
| Legacy-Route | Methode | Neu | Ressource |
|---|---|---|---|
pressrelease/create |
POST | POST /v1/press-releases |
PressReleaseResource |
pressrelease/list |
GET | GET /v1/press-releases?page=&status= |
collection |
pressrelease/show/:id |
GET | GET /v1/press-releases/{id} |
|
pressrelease/update/:id |
POST | PATCH /v1/press-releases/{id} |
|
pressrelease/delete/:id |
POST | DELETE /v1/press-releases/{id} |
|
pressreleaseimage/upload |
POST | POST /v1/press-releases/{id}/images |
multipart/form-data, max. 5 MB |
pressreleaseimage/list |
GET | GET /v1/press-releases/{id}/images |
collection |
pressreleaseimage/delete/:id |
POST | DELETE /v1/press-release-images/{id} |
|
company/list |
GET | GET /v1/companies |
|
company/show/:id |
GET | GET /v1/companies/{id} |
|
category/list |
GET | GET /v1/categories?lang=de|en |
|
newsletter/subscribe |
POST | POST /v1/newsletter/subscribe |
|
newsletter/unsubscribe |
– | nicht Teil von P7 | wird später im neuen Frontend mit sauberem Newsletter-Dienst neu implementiert |
Beispiel: PressReleaseResource
return [
'id' => $this->id,
'uuid' => $this->uuid,
'legacy' => [
'portal' => $this->legacy_portal,
'id' => $this->legacy_id,
],
'portal' => $this->portal->value,
'title' => $this->title,
'slug' => $this->slug,
'language' => $this->language,
'status' => $this->status->value,
'text' => $this->text,
'backlink_url' => $this->backlink_url,
'keywords' => $this->keywords,
'teaser' => [
'begin' => $this->teaser_begin,
'end' => $this->teaser_end,
],
'category' => CategoryResource::make($this->whenLoaded('category')),
'company' => CompanyResource::make($this->whenLoaded('company')),
'images' => PressReleaseImageResource::collection($this->whenLoaded('images')),
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
];
5. Authentifizierung (Sanctum only)
Route-Definition:
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::apiResource('press-releases', PressReleaseController::class);
Route::get('press-releases/{pressRelease}/images', [PressReleaseImageController::class, 'index']);
Route::post('press-releases/{pressRelease}/images', [PressReleaseImageController::class, 'store']);
Route::delete('press-release-images/{pressReleaseImage}', [PressReleaseImageController::class, 'destroy']);
Route::apiResource('companies', CompanyController::class)->only(['index', 'show']);
Route::get('categories', [CategoryController::class, 'index']);
Route::post('newsletter/subscribe', [NewsletterController::class, 'subscribe']);
});
Abilities werden pro Controller-Action geprüft:
$request->user()->tokenCan('press-releases:write') or abort(403);
Legacy-Handler (One-Shot)
Ein kleiner Handler fängt Requests mit api_key-Param ab und antwortet mit einer freundlichen Fehlermeldung inkl. Link zur Migrationsdoku (statt generischem 401):
if ($request->has('api_key') || $request->header('X-Api-Key')) {
return response()->json([
'message' => 'Legacy API keys are no longer supported.',
'migration_url' => url('/account/tokens'),
'docs_url' => url('/docs/api/v1'),
], 410); // 410 Gone – signalisiert deutlich: "Feature ist weg, hier ist der Ersatz"
}
6. Authorization (Policies)
PressReleasePolicy::create→ User musspress-releases:write-Ability UND Mitglied einer Company mitrole∈ {owner, responsible} sein.PressReleasePolicy::update/delete→ Ownership + Status-Check (nurdraft/rejectededitierbar).PressReleaseImageController→ List nur eigene Pressemitteilung, Upload/Delete nur eigenedraft/rejectedPressemitteilung, Upload max. 5 MB.NewsletterController::subscribe→ öffentlich, abernewsletter:subscribe-Ability erforderlich bei authentifiziertem Zugriff.newsletter/unsubscribe→ bewusst nicht in P7 umgesetzt; neuer Newsletter-Dienst folgt nach der Migration.- Fehlermeldungen sprachlich konsistent (deutsch im
message-Feld, abhängig vonAccept-Language).
7. Fehlerformat
Standard Laravel:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"message": "The given data was invalid.",
"errors": {
"title": ["Der Titel wird benötigt."],
"text": ["Der Text darf nicht leer sein."]
}
}
Legacy-Key-Versuch:
HTTP/1.1 410 Gone
Content-Type: application/json
{
"message": "Legacy API keys are no longer supported.",
"migration_url": "https://presseportale.com/account/tokens",
"docs_url": "https://presseportale.com/docs/api/v1"
}
8. Versioning & Zukunft
v1= neuer Stand ab Go-Live (keine Legacy-Altlasten).v2erst auf Anfrage / bei breaking changes.
9. Testing
- Feature-Tests pro Endpoint:
- Happy-Path mit Sanctum-Token (mit passender Ability)
- Auth-Fehlerfall (kein Token)
- Ability-Fehlerfall (403)
- Legacy-Key-Versuch → 410 mit migration_url
- Validation-Fehler (422)
- Policy-Fehler (403)
- Smoke-Test-Skript (
php artisan api:smoke) das gegen Staging 10 kritische Endpoints fährt.
10. OpenAPI-Dokumentation
Ablegen unter docs/api/v1.yml, generiert aus:
- Scribe (
knuckleswtf/scribe) – PHP-Annotations → YAML, oder - manuell (konsistenter, wenn Team es pflegt).
Empfehlung: Mit Scribe starten, bei Bedarf manuell überschreiben.
11. Migrationsschritte in Phase 7
- Log-Analyse: alle in den letzten 90 Tagen in Produktion aufgerufenen URLs auflisten
- Endpoints priorisieren (Top-20 zuerst)
- Routen, Controller, Resources, Requests, Policies pro Endpoint
- Sanctum-Ability-Seeder für Standard-Scopes
- Legacy-410-Handler
- Integrationstests
- Smoke-Tests gegen Staging mit echtem Client
- Kommunikation an API-Partner (E-Mail-Verteiler) mit Token-Migrations-Anleitung (T-30, T-7, T-0)
- Self-Service Token-Management im Customer-Portal verfügbar machen
11.1 Log-Analyse-Tooling
Für Schritt 1 gibt es ein wiederholbares Artisan-Werkzeug:
php artisan api:analyze-legacy-access-logs /var/log/nginx/access.log "/var/log/nginx/access.log.*" --top=20
Der Command erkennt Legacy-API-Routen (pressrelease/*, pressreleaseimage/*, company/*, category/*, newsletter/*), zählt Requests mit api_key, maskiert Beispielrequests und speichert nur API-Key-Fingerprints statt Klartext-Keys.
Der JSON-Report landet unter:
storage/app/private/migration/legacy-api-access-*.json