Architecture REST

MéthodeURLActionCode succÚs
GET/api/v1/tasksLister (paginé)200
POST/api/v1/tasksCréer201
GET/api/v1/tasks/{id}Détail200
PUT/PATCH/api/v1/tasks/{id}Modifier200
DELETE/api/v1/tasks/{id}Supprimer204
// routes/api.php — versioning
use Illuminate\Support\Facades\Route;

Route::prefix('v1')->group(function() {
    // Public
    Route::post('/auth/register', [AuthController::class, 'register']);
    Route::post('/auth/login',    [AuthController::class, 'login']);

    // Protégé Sanctum
    Route::middleware('auth:sanctum')->group(function() {
        Route::get('/auth/me', [AuthController::class, 'me']);
        Route::post('/auth/logout', [AuthController::class, 'logout']);

        Route::apiResource('tasks',    TaskController::class);
        Route::apiResource('projects', ProjectController::class);

        // Nested resource
        Route::apiResource('projects.tasks', ProjectTaskController::class)
            ->shallow();
    });
});

API Resources

php artisan make:resource TaskResource
php artisan make:resource TaskCollection  # collection avec méta
// app/Http/Resources/TaskResource.php
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class TaskResource extends JsonResource
{
    public function toArray(Request $request): array {
        return [
            'id'          => $this->id,
            'title'       => $this->title,
            'status'      => $this->status,
            'priority'    => $this->priority,
            'is_overdue'  => $this->when($this->isOverdue(), true),
            'created_at'  => $this->created_at->toIso8601String(),

            // Relation incluse seulement si eager-loadée
            'project'     => new ProjectResource($this->whenLoaded('project')),
            'assignee'    => new UserResource($this->whenLoaded('assignee')),
            'comments_count' => $this->when(
                $this->relationLoaded('comments'),
                fn() => $this->comments->count()
            ),
        ];
    }
}

// Collection avec méta custom
class TaskCollection extends ResourceCollection
{
    public function toArray(Request $request): array {
        return [
            'data' => $this->collection,
            'meta' => [
                'total'      => $this->total(),
                'per_page'   => $this->perPage(),
                'last_page'  => $this->lastPage(),
                'filters'    => $request->only(['status', 'priority']),
            ],
        ];
    }
}
// Utilisation dans le controller
public function index(Request $request): AnonymousResourceCollection {
    $tasks = Task::with(['project', 'assignee'])
        ->when($request->status, fn($q) => $q->where('status', $request->status))
        ->paginate($request->per_page ?? 15);

    return TaskResource::collection($tasks);
}

public function store(StoreTaskRequest $request): TaskResource {
    $task = Task::create($request->validated());
    return (new TaskResource($task))->response()->setStatusCode(201);
}

public function show(Task $task): TaskResource {
    $task->load('project', 'assignee', 'comments');
    return new TaskResource($task);
}

Form Requests

php artisan make:request StoreTaskRequest
php artisan make:request UpdateTaskRequest
// app/Http/Requests/StoreTaskRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool {
        // Vérifier que l'utilisateur peut créer une tùche dans ce projet
        $project = $this->route('project');
        return $project === null || $this->user()->can('update', $project);
    }

    public function rules(): array {
        return [
            'title'       => 'required|string|max:255',
            'body'        => 'nullable|string',
            'status'      => 'in:todo,in_progress,done',
            'priority'    => 'in:low,medium,high',
            'due_date'    => 'nullable|date|after:today',
            'assignee_id' => 'nullable|exists:users,id',
            'tags'        => 'array',
            'tags.*'      => 'string|max:50',
        ];
    }

    public function messages(): array {
        return [
            'title.required'     => 'Le titre est obligatoire.',
            'title.max'          => 'Le titre ne doit pas dépasser 255 caractÚres.',
            'due_date.after'     => 'La date d\'Ă©chĂ©ance doit ĂȘtre dans le futur.',
            'assignee_id.exists' => 'L\'utilisateur assigné n\'existe pas.',
        ];
    }

    public function attributes(): array {
        return [
            'title'       => 'titre',
            'body'        => 'description',
            'due_date'    => 'date d\'échéance',
            'assignee_id' => 'assigné',
        ];
    }

    // Personnaliser la réponse 422 (pour une API)
    protected function failedValidation(Validator $validator): void {
        throw new HttpResponseException(
            response()->json([
                'message' => 'Données invalides.',
                'errors'  => $validator->errors(),
            ], 422)
        );
    }
}

Sanctum — Token abilities

// Créer un token avec abilities (scopes)
$token = $user->createToken('mobile-app', ['tasks:read', 'tasks:write']);

// Vérifier une ability
$request->user()->tokenCan('tasks:write');

// Définir les abilities dans les routes
Route::middleware(['auth:sanctum', 'abilities:tasks:read'])->group(function() {
    Route::get('/tasks', [TaskController::class, 'index']);
});

Route::middleware(['auth:sanctum', 'ability:tasks:write'])->group(function() {
    Route::post('/tasks', [TaskController::class, 'store']);
});

// Révoquer
$user->currentAccessToken()->delete();  // token courant
$user->tokens()->where('name', 'mobile-app')->delete();  // par nom
$user->tokens()->delete();              // tous

Versioning API

// bootstrap/app.php
->withRouting(
    api:        __DIR__.'/../routes/api.php',     // /api/v1
    health:     '/up',
)

// routes/api.php
Route::prefix('v1')->name('api.v1.')->group(base_path('routes/api_v1.php'));
Route::prefix('v2')->name('api.v2.')->group(base_path('routes/api_v2.php'));

Gestion d'erreurs JSON

// bootstrap/app.php — centraliser les erreurs
->withExceptions(function (Exceptions $exceptions) {

    $exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'Non authentifié.'], 401);
        }
    });

    $exceptions->render(function (\Illuminate\Auth\Access\AuthorizationException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'AccÚs refusé.'], 403);
        }
    });

    $exceptions->render(function (\Illuminate\Database\Eloquent\ModelNotFoundException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Ressource introuvable.',
                'model'   => class_basename($e->getModel()),
            ], 404);
        }
    });

    $exceptions->render(function (\Illuminate\Validation\ValidationException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Données invalides.',
                'errors'  => $e->errors(),
            ], 422);
        }
    });
})

Documentation API

# Option 1 : Scramble (plus simple, auto-génération)
composer require dedoc/scramble
# → /docs/api automatiquement

# Option 2 : L5-Swagger (annotations OpenAPI)
composer require darkaonline/l5-swagger
php artisan l5-swagger:generate
// Scramble — configuration dans config/scramble.php
return [
    'api_path'      => 'api/v1',
    'info' => [
        'version' => '1.0.0',
        'title'   => 'TaskManager API',
    ],
];
← Module 05 ▶ Mini-projet 🧠 QCM Module 07 →