Ir al contenido principal
Laravel, shipping fast.
// app/Http/Controllers/ConsumerController.php
public function destroy(Consumer $consumer): Response
{
    DB::transaction(function () use ($consumer) {
        $consumer->tokens()->delete();
        $consumer->forceFill(['settings' => null])->save();
        $consumer->delete();
    });

    return response()->noContent();
}

Un 204 dice «hecho, y no hay nada que devolver». Compáralo con el 202 de DELETE /licenses/{license}: aquel prometía trabajo para más tarde. Este ha terminado cuando responde, así que lo dice.

El orden sigue lo peligroso que es conservar cada cosa. Primero se van los tokens, para que el consumidor ya no pueda autenticarse. Después los ajustes, porque guardan la credencial de proveedor de un socio, y no hay razón para conservar un secreto de una cuenta que se ha ido. forceFill() hace falta porque settings no es asignable en bloque.

Por último, la fila misma. Como el modelo usa SoftDeletes, delete() fija deleted_at y deja la fila. A partir de ahí todas las consultas la ignoran: desaparece de la lista y el route model binding responde 404.

El borrado lógico está aquí por una sola razón. Eliminar un consumidor por error tumba la integración de un socio, y con la fila todavía ahí, un operador puede hacerle restore() con el mismo ID y el mismo nombre, y después emitir tokens nuevos y volver a introducir las credenciales. No es una política de retención. Una fila borrada lógicamente sigue siendo un dato que conservas, y el Capítulo 17 la elimina de forma definitiva tras un plazo fijo.

Probar datos que son tuyos

Con una base de datos detrás del endpoint, los tests comprueban dos cosas: la respuesta, y lo que ahora es verdad en la tabla.

// tests/Feature/ConsumerTest.php
it('lets an operator create a consumer', function () {
    Sanctum::actingAs(
        Consumer::factory()->create(),
        ['consumers:manage'],
    );

    $this->postJson('/api/consumers', ['name' => 'Partner A'])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Partner A');

    $this->assertDatabaseHas('consumers', [
        'name' => 'Partner A',
    ]);
});

Añade RefreshDatabase al caso de test y cada test parte de un esquema vacío. (Documentación de Laravel: Database Testing.)

Después, los tests en los que pregunta quien no debe. Son los que nadie escribe, porque la funcionalidad funciona sin ellos:

it('hides other consumers from a consumer', function () {
    $other = Consumer::factory()->create();

    Sanctum::actingAs(
        Consumer::factory()->create(),
        ['licenses:read'],
    );

    $this->getJson("/api/consumers/{$other->id}")
        ->assertNotFound();
});
it('refuses to issue tokens to a non-operator', function () {
    $other = Consumer::factory()->create();

    Sanctum::actingAs(
        Consumer::factory()->create(),
        ['licenses:write'],
    );

    $this->postJson("/api/consumers/{$other->id}/tokens", [
        'name' => 'stolen',
        'abilities' => ['consumers:manage'],
    ])->assertForbidden();

    expect($other->tokens()->count())->toBe(0);
});

La última línea del segundo test importa tanto como el código de estado. Un 403 que aun así hubiera creado el token pasaría un test que solo mirara la respuesta. El estado sería el mismo para un consumidor que no existe, porque la habilidad se comprueba antes de cargar el registro.

Y uno para el secreto:

it('never exposes settings', function () {
    $consumer = Consumer::factory()
        ->forPartner('secret-token')
        ->create();

    Sanctum::actingAs($consumer, ['licenses:read']);

    $this->getJson("/api/consumers/{$consumer->id}")
        ->assertOk()
        ->assertJsonMissingPath('data.settings')
        ->assertDontSee('secret-token');
});

Lo que fue distinto

Pon este capítulo al lado de los capítulos 2 y 3.

Las partes que dan la cara al consumidor son idénticas: un FormRequest en la frontera, un controlador delgado, un Resource que actúa de lista blanca, data en el sobre, los mismos errores. Un cliente no puede distinguir cuáles de los recursos de esta API viven en una base de datos y cuáles en un proveedor. Esa es la consistencia de la que hablaba el prefacio.

Todo lo que hay detrás del controlador cambió. Donde las licencias necesitaban un contrato, un driver y un objeto tipado, los consumidores necesitaron un modelo. El route model binding reemplazó a la búsqueda. Los casts reemplazaron al objeto tipado. El paginador no reemplazó nada, porque el proveedor nunca ofreció uno.

Resumen del capítulo 5

  • Un recurso respaldado por una tabla necesita un modelo, un FormRequest por acción, un Resource y una Policy para lo que dependa del registro. Empieza con apiResource y no nombres nada a mano.
  • Dales ámbito a los recursos anidados, para que un hijo en la URL tenga que pertenecer a su padre.
  • Deja que el route model binding haga la búsqueda y el 404.
  • Pásale validated() al modelo, nunca all(). Mantén los secretos fuera de Fillable y márcalos como Hidden.
  • Respalda una regla unique con un índice único. Usa sometimes e ignore() en las actualizaciones.
  • En un Resource, usa whenLoaded y whenCounted, para que nunca pueda ejecutar una consulta.
  • Valida los parámetros de consulta. Pon las columnas de orden en una lista blanca. Acota el tamaño de página por los dos lados.
  • Comprueba las habilidades en middleware, antes de que se cargue un registro. Pon las reglas por registro en una Policy, y responde 404 cuando quien llama no debería saber que el registro existe.
  • Todo controlador nuevo declara quién puede llamarlo. Un endpoint que emite credenciales, más que ninguno.
  • Quien llama solo puede conceder las habilidades que tiene.
  • Borra del todo las credenciales y los secretos. Borra de forma lógica solo lo que quizá necesites restaurar.
  • Un 204 significa hecho. Un 202 significa más tarde. Di el que sea verdad.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.