Modern PHP Engineering
Chapitre 8
08 — APIs PHP
08 — APIs PHP
Cours 08 — APIs PHP
08.1 REST API Design
Resource Naming
GET /users → Liste d'utilisateurs
POST /users → Créer un utilisateur
GET /users/{id} → Détail d'un utilisateur
PUT /users/{id} → Remplacer un utilisateur
PATCH /users/{id} → Modifier partiellement
DELETE /users/{id} → Supprimer
GET /users/{id}/posts → Posts d'un utilisateur
POST /users/{id}/posts → Créer un post pour un utilisateur
GET /posts → Liste des posts
GET /posts/{id} → Détail d'un post
GET /posts/{id}/comments → Commentaires d'un post
Pagination
<?php
// Standard : offset/limit ou cursor-based
// Réponse type :
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 150,
"last_page": 8,
"from": 1,
"to": 20
},
"links": {
"first": "https://api.example.com/users?page=1",
"last": "https://api.example.com/users?page=8",
"prev": null,
"next": "https://api.example.com/users?page=2"
}
}
Filtering, Sorting
# Filtering
GET /users?filter[name]=John
GET /users?filter[created_at][gt]=2025-01-01
GET /users?filter[role]=admin,editor
# Sorting
GET /users?sort=name
GET /users?sort=-created_at (desc)
GET /users?sort=name,created_at
# Include (eager loading)
GET /users?include=posts,roles
# Fields (sparse fields)
GET /users?fields[users]=id,name,email
GET /users?include=posts&fields[posts]=id,title
HATEOAS
<?php
// Hypermedia As The Engine Of Application State
// Chaque réponse inclut des liens pour naviguer dans l'API
function hateoasResponse(array $data, array $links): array
{
return [
'data' => $data,
'_links' => $links,
];
}
// Exemple pour /users/1
$response = hateoasResponse(
data: [
'id' => 1,
'name' => 'Alice',
'email' => 'alice@example.com',
],
links: [
'self' => ['href' => '/api/users/1', 'method' => 'GET'],
'posts' => ['href' => '/api/users/1/posts', 'method' => 'GET'],
'update' => ['href' => '/api/users/1', 'method' => 'PUT'],
'delete' => ['href' => '/api/users/1', 'method' => 'DELETE'],
]
);
08.2 Laravel Sanctum
Sanctum est le package d'authentification API de Laravel. Léger et flexible.
Installation
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
SPA Authentication
<?php
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:3000')),
'guard' => ['web'],
'expiration' => null,
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
],
];
// Sanctum utilise les cookies de session pour les SPAs
// Pas de token, juste CSRF + session
// Sanctum::actingAs($user) dans les tests
Token Authentication (API)
<?php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
Route::post('/sanctum/token', function (Request $request) {
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required|string',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['Les identifiants sont incorrects.'],
]);
}
return $user->createToken($request->device_name)->plainTextToken;
});
// Protéger une route
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Token Abilities
<?php
// Créer un token avec des capacités
$token = $user->createToken('mobile', ['posts:read', 'posts:write']);
$token = $user->createToken('dashboard', ['admin:read', 'admin:write']);
// Vérifier les capacités
if ($request->user()->tokenCan('posts:write')) {
// Peut créer/modifier des posts
}
// Route avec middleware
Route::middleware('auth:sanctum', 'abilities:posts:write')->post('/posts', function () {
// Seulement si le token a la capacité posts:write
});
Route::middleware('auth:sanctum', 'ability:admin:read,admin:write')->group(function () {
// Routes admin
});
Supprimer des tokens
<?php
// Supprimer le token courant (déconnexion)
$request->user()->currentAccessToken()->delete();
// Supprimer tous les tokens
$request->user()->tokens()->delete();
// Supprimer un token spécifique
$user->tokens()->where('id', $tokenId)->delete();
08.3 API Platform
API Platform est un framework d'API construit sur Symfony.
Installation
composer create-project api-platform/api-platform
# ou dans un projet Symfony existant :
composer require api-platform
Resources
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\ApiFilter;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
new Post(),
new Put(),
new Delete(),
],
normalizationContext: ['groups' => ['book:read']],
denormalizationContext: ['groups' => ['book:write']],
paginationItemsPerPage: 20,
paginationClientItemsPerPage: true,
)]
#[ApiFilter(SearchFilter::class, properties: ['title' => 'partial', 'author.name' => 'partial'])]
#[ApiFilter(OrderFilter::class, properties: ['id', 'title', 'createdAt'])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
class Book
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column]
#[Assert\NotBlank]
#[Groups(['book:read', 'book:write'])]
private string $title;
#[ORM\Column(type: 'text')]
#[Groups(['book:read', 'book:write'])]
private string $description;
#[ORM\Column(type: 'date')]
#[Groups(['book:read', 'book:write'])]
private \DateTimeInterface $publishedAt;
#[ORM\ManyToOne(inversedBy: 'books')]
#[Groups(['book:read', 'book:write'])]
private ?Author $author = null;
// Getters/Setters...
}
Filters
<?php
// SearchFilter : partial, exact, start, end, word_start
// DateFilter : before, after, strictly_before, strictly_after
// OrderFilter : ASC, DESC
// RangeFilter : gt, gte, lt, lte
// BooleanFilter : true, false
// NumericFilter : opérateurs numériques
// ExistsFilter : null/non-null
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
#[ApiFilter(RangeFilter::class, properties: ['price'])]
#[ApiFilter(SearchFilter::class, properties: [
'title' => 'partial',
'author.name' => 'partial',
'isbn' => 'exact',
])]
Custom Operations
<?php
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Symfony\Action\NotFoundAction;
#[ApiResource(
operations: [
new Get(controller: NotFoundAction::class, read: false), // Désactiver GET
new Post(
uriTemplate: '/books/{id}/publish',
status: 202,
messenger: true, // Message async
),
],
)]
class Book
{
// ...
}
08.4 GraphQL avec Lighthouse
Lighthouse est un serveur GraphQL pour Laravel.
Installation
composer require nuwave/lighthouse
php artisan vendor:publish --tag=lighthouse-config
php artisan lighthouse:ide-helper
Schema
# graphql/schema.graphql
type Query {
users: [User!]! @paginate
user(id: ID! @eq): User @find
posts(
title: String @where(operator: "like")
published: Boolean @eq
orderBy: _ @orderBy
): [Post!]! @paginate
}
type Mutation {
createUser(input: CreateUserInput! @spread): User! @create
updateUser(id: ID!, input: UpdateUserInput! @spread): User! @update
deleteUser(id: ID!): User! @delete
login(email: String!, password: String!): AuthPayload! @field(resolver: "AuthMutator@login")
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]! @hasMany
createdAt: DateTime!
updatedAt: DateTime!
}
type Post {
id: ID!
title: String!
body: String!
user: User! @belongsTo
comments: [Comment!]! @hasMany
createdAt: DateTime!
}
type Comment {
id: ID!
body: String!
user: User! @belongsTo
post: Post! @belongsTo
}
input CreateUserInput {
name: String! @rules(apply: ["min:2", "max:255"])
email: String! @rules(apply: ["email", "unique:users,email"])
password: String! @rules(apply: ["min:8"]) @hash
}
type AuthPayload {
token: String!
user: User!
}
Resolvers
<?php
namespace App\GraphQL\Mutations;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class AuthMutator
{
public function login($root, array $args): array
{
$user = User::where('email', $args['email'])->first();
if (!$user || !Hash::check($args['password'], $user->password)) {
throw new \Exception('Identifiants incorrects');
}
return [
'token' => $user->createToken('graphql')->plainTextToken,
'user' => $user,
];
}
}
Directives
# Directives principales Lighthouse
@paginate # Pagination automatique (paginator)
@find # Find par ID
@create # CREATE mutation
@update # UPDATE mutation
@delete # DELETE mutation
@upsert # INSERT OR UPDATE
@hasMany # Relation HasMany
@belongsTo # Relation BelongsTo
@eq # WHERE column = value
@where # WHERE personnalisé
@orderBy # ORDER BY dynamique
@rules # Validation
@hash # Hash un champ (password)
@trim # Trim une string
@spread # Spread un input
@middleware # Middleware (auth:api)
@guard # Guard
@can # Policy
@softDeletes # Soft deletes
Validation
input CreatePostInput {
title: String! @rules(apply: ["required", "min:5", "max:255"])
body: String! @rules(apply: ["required", "min:10"])
user_id: ID! @rules(apply: ["exists:users,id"])
}
Pagination GraphQL
# Connection-based pagination (Relay spec)
query {
posts(first: 10, page: 1) {
data {
id
title
}
paginatorInfo {
total
currentPage
lastPage
hasMorePages
}
}
}
08.5 Rate Limiting
Laravel Rate Limiter
<?php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
// Définir un limiteur
RateLimiter::for('api', fn(Request $request) =>
Limit::perMinute(60)->by($request->user()?->id ?: $request->ip())
);
RateLimiter::for('login', fn(Request $request) =>
Limit::perMinute(5)->by($request->input('email') . '|' . $request->ip())
);
RateLimiter::for('uploads', fn(Request $request) =>
Limit::perHour(10)->by($request->user()->id)
);
// Appliquer sur les routes
Route::middleware('throttle:api')->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
Route::middleware('throttle:login')->post('/login', [AuthController::class, 'login']);
// Vérifier manuellement
if (RateLimiter::tooManyAttempts('send-message:' . $user->id, 5)) {
$seconds = RateLimiter::availableIn('send-message:' . $user->id);
return response()->json([
'message' => "Trop de tentatives. Réessayez dans {$seconds} secondes.",
], 429);
}
RateLimiter::hit('send-message:' . $user->id, 60);
API Platform Rate Limiting
# config/packages/api_platform.yaml
api_platform:
defaults:
extra_properties:
rfc_7807_compliant_errors: true
# Rate limiting via Symfony
Headers de rate limiting
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1623456789
Retry-After: 120
Exercices
- Concevez une API REST complète pour un blog (resources, pagination, filtres)
- Implémentez Sanctum avec token abilities
- Créez une ressource API Platform avec filtres
- Écrivez un schema GraphQL avec mutations et validation
- Mettez en place du rate limiting avec retry-after
Références
- laravel.com/docs/sanctum
- api-platform.com/docs
- lighthouse-php.com
- restfulapi.net
- graphql.org/learn