PHP API Development
Building Modern APIs with PHP
Explanation
PHP API Frameworks
PHP has evolved into a powerful language for API development. Modern frameworks like Laravel and Slim make building APIs straightforward and maintainable.
Framework Comparison
| Feature | Laravel | Slim | Symfony | |---------|---------|------|---------| | Learning Curve | Moderate | Low | High | | Features | Full-stack | Minimal | Enterprise | | Performance | Good | Fast | Good | | Best For | Most apps | Microservices | Large apps |
Demonstration
Example 1: Laravel API Basics
<?php
// routes/api.php
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\PostController;
Route::prefix('v1')->group(function () {
// Public routes
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('users', UserController::class);
Route::apiResource('posts', PostController::class);
Route::get('/posts/{post}/comments', [CommentController::class, 'index']);
Route::post('/posts/{post}/comments', [CommentController::class, 'store']);
});
});
// app/Http/Controllers/Api/UserController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\UserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function index(Request $request)
{
$users = User::query()
->when($request->role, fn($q, $role) => $q->where('role', $role))
->when($request->search, fn($q, $search) =>
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
)
->paginate($request->per_page ?? 15);
return UserResource::collection($users);
}
public function store(UserRequest $request)
{
$user = User::create($request->validated());
return new UserResource($user);
}
public function show(User $user)
{
return new UserResource($user->load('posts', 'comments'));
}
public function update(UserRequest $request, User $user)
{
$user->update($request->validated());
return new UserResource($user);
}
public function destroy(User $user)
{
$user->delete();
return response()->noContent();
}
}
Example 2: API Resources and Collections
<?php
// app/Http/Resources/UserResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'avatar_url' => $this->avatar_url,
'role' => $this->role,
'created_at' => $this->created_at->toISOString(),
// Conditional attributes
'email_verified_at' => $this->when(
$request->user()?->isAdmin(),
$this->email_verified_at
),
// Relationships
'posts' => PostResource::collection($this->whenLoaded('posts')),
'posts_count' => $this->when(
$this->posts_count !== null,
$this->posts_count
),
// Links
'links' => [
'self' => route('users.show', $this->id),
'posts' => route('users.posts', $this->id),
],
];
}
public function with(Request $request): array
{
return [
'meta' => [
'version' => '1.0',
],
];
}
}
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => str($this->body)->limit(150),
'body' => $this->when(
$request->routeIs('posts.show'),
$this->body
),
'published_at' => $this->published_at?->toISOString(),
'reading_time' => $this->reading_time,
'author' => new UserResource($this->whenLoaded('author')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'comments_count' => $this->whenCounted('comments'),
];
}
}
// app/Http/Resources/UserCollection.php
class UserCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
}
Example 3: Authentication with Sanctum
<?php
// app/Http/Controllers/Api/AuthController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\RegisterRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function register(RegisterRequest $request)
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'user' => new UserResource($user),
'token' => $token,
], 201);
}
public function login(LoginRequest $request)
{
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
// Revoke previous tokens (optional)
$user->tokens()->delete();
$token = $user->createToken('auth-token', $this->getAbilities($user))
->plainTextToken;
return response()->json([
'user' => new UserResource($user),
'token' => $token,
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out']);
}
public function me(Request $request)
{
return new UserResource($request->user()->load('posts'));
}
private function getAbilities(User $user): array
{
$abilities = ['read'];
if ($user->isAdmin()) {
$abilities = array_merge($abilities, ['create', 'update', 'delete']);
}
return $abilities;
}
}
// Middleware for ability checking
// app/Http/Middleware/CheckAbility.php
class CheckAbility
{
public function handle(Request $request, Closure $next, string $ability)
{
if (!$request->user()->tokenCan($ability)) {
abort(403, 'Insufficient permissions');
}
return $next($request);
}
}
// Usage in routes
Route::middleware(['auth:sanctum', 'ability:delete'])
->delete('/posts/{post}', [PostController::class, 'destroy']);
Example 4: Slim Framework API
<?php
// public/index.php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
$app->addErrorMiddleware(true, true, true);
// Middleware
$app->add(function (Request $request, $handler): Response {
$response = $handler->handle($request);
return $response
->withHeader('Content-Type', 'application/json')
->withHeader('Access-Control-Allow-Origin', '*');
});
// Routes
$app->get('/api/users', function (Request $request, Response $response) {
$users = Database::query('SELECT * FROM users');
$response->getBody()->write(json_encode([
'data' => $users
]));
return $response;
});
$app->get('/api/users/{id}', function (Request $request, Response $response, array $args) {
$user = Database::query('SELECT * FROM users WHERE id = ?', [$args['id']]);
if (!$user) {
return $response
->withStatus(404)
->getBody()
->write(json_encode(['error' => 'User not found']));
}
$response->getBody()->write(json_encode(['data' => $user]));
return $response;
});
$app->post('/api/users', function (Request $request, Response $response) {
$data = $request->getParsedBody();
// Validation
$errors = validate($data, [
'name' => 'required|min:2',
'email' => 'required|email|unique:users',
]);
if ($errors) {
$response->getBody()->write(json_encode(['errors' => $errors]));
return $response->withStatus(422);
}
$id = Database::insert('users', $data);
$user = Database::find('users', $id);
$response->getBody()->write(json_encode(['data' => $user]));
return $response->withStatus(201);
});
$app->put('/api/users/{id}', function (Request $request, Response $response, array $args) {
$data = $request->getParsedBody();
Database::update('users', $args['id'], $data);
$user = Database::find('users', $args['id']);
$response->getBody()->write(json_encode(['data' => $user]));
return $response;
});
$app->delete('/api/users/{id}', function (Request $request, Response $response, array $args) {
Database::delete('users', $args['id']);
return $response->withStatus(204);
});
$app->run();
Example 5: Error Handling
<?php
// app/Exceptions/Handler.php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class Handler extends ExceptionHandler
{
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
return $this->handleApiException($request, $e);
}
return parent::render($request, $e);
}
private function handleApiException($request, Throwable $e)
{
if ($e instanceof ValidationException) {
return response()->json([
'error' => [
'code' => 'VALIDATION_ERROR',
'message' => 'The given data was invalid.',
'details' => $e->errors(),
],
], 422);
}
if ($e instanceof AuthenticationException) {
return response()->json([
'error' => [
'code' => 'UNAUTHENTICATED',
'message' => 'Unauthenticated.',
],
], 401);
}
if ($e instanceof HttpException) {
return response()->json([
'error' => [
'code' => $this->getErrorCode($e->getStatusCode()),
'message' => $e->getMessage() ?: 'An error occurred.',
],
], $e->getStatusCode());
}
// Log the actual error
report($e);
// Return generic error in production
return response()->json([
'error' => [
'code' => 'SERVER_ERROR',
'message' => config('app.debug')
? $e->getMessage()
: 'An unexpected error occurred.',
],
], 500);
}
private function getErrorCode(int $statusCode): string
{
return match ($statusCode) {
400 => 'BAD_REQUEST',
401 => 'UNAUTHORIZED',
403 => 'FORBIDDEN',
404 => 'NOT_FOUND',
405 => 'METHOD_NOT_ALLOWED',
422 => 'UNPROCESSABLE_ENTITY',
429 => 'TOO_MANY_REQUESTS',
default => 'ERROR',
};
}
}
// Custom exceptions
// app/Exceptions/ApiException.php
class ApiException extends \Exception
{
protected string $errorCode;
protected array $details;
public function __construct(
string $message,
string $errorCode,
int $statusCode = 400,
array $details = []
) {
parent::__construct($message, $statusCode);
$this->errorCode = $errorCode;
$this->details = $details;
}
public function render()
{
return response()->json([
'error' => [
'code' => $this->errorCode,
'message' => $this->getMessage(),
'details' => $this->details ?: null,
],
], $this->getCode());
}
}
// Usage
throw new ApiException(
'Insufficient balance',
'INSUFFICIENT_BALANCE',
400,
['current_balance' => 50, 'required' => 100]
);
Example 6: API Testing
<?php
// tests/Feature/Api/UserTest.php
namespace Tests\Feature\Api;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserTest extends TestCase
{
use RefreshDatabase;
private User $user;
private string $token;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->token = $this->user->createToken('test')->plainTextToken;
}
public function test_can_list_users(): void
{
User::factory()->count(10)->create();
$response = $this->withToken($this->token)
->getJson('/api/v1/users');
$response->assertOk()
->assertJsonCount(11, 'data')
->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'email', 'created_at'],
],
'meta' => ['current_page', 'total'],
]);
}
public function test_can_create_user(): void
{
$data = [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
];
$response = $this->withToken($this->token)
->postJson('/api/v1/users', $data);
$response->assertCreated()
->assertJson([
'data' => [
'name' => 'John Doe',
'email' => 'john@example.com',
],
]);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com',
]);
}
public function test_validation_errors_returned_correctly(): void
{
$response = $this->withToken($this->token)
->postJson('/api/v1/users', [
'name' => '',
'email' => 'invalid',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['name', 'email', 'password']);
}
public function test_unauthenticated_request_returns_401(): void
{
$response = $this->getJson('/api/v1/users');
$response->assertUnauthorized();
}
}
Key Takeaways:
- Laravel excels for full-featured APIs
- Use API Resources for consistent responses
- Sanctum provides simple token authentication
- Proper error handling is crucial
- Always test your API endpoints
Imitation
Challenge 1: Build a Task API
Task: Create a RESTful API for managing tasks with authentication and validation.
Solution
<?php
// app/Http/Controllers/Api/TaskController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\TaskRequest;
use App\Http\Resources\TaskResource;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index(Request $request)
{
$tasks = $request->user()
->tasks()
->when($request->status, fn($q, $status) => $q->where('status', $status))
->when($request->priority, fn($q, $p) => $q->where('priority', $p))
->orderBy($request->sort ?? 'created_at', $request->order ?? 'desc')
->paginate($request->per_page ?? 15);
return TaskResource::collection($tasks);
}
public function store(TaskRequest $request)
{
$task = $request->user()->tasks()->create($request->validated());
return new TaskResource($task);
}
public function show(Task $task)
{
$this->authorize('view', $task);
return new TaskResource($task->load('subtasks', 'tags'));
}
public function update(TaskRequest $request, Task $task)
{
$this->authorize('update', $task);
$task->update($request->validated());
return new TaskResource($task);
}
public function destroy(Task $task)
{
$this->authorize('delete', $task);
$task->delete();
return response()->noContent();
}
public function complete(Task $task)
{
$this->authorize('update', $task);
$task->update(['status' => 'completed', 'completed_at' => now()]);
return new TaskResource($task);
}
}
Practice
Exercise 1: API Rate Limiting
Difficulty: Intermediate
Implement custom rate limiting:
- Different limits per endpoint
- User-specific quotas
- Rate limit headers
Exercise 2: API Caching
Difficulty: Advanced
Add caching layer:
- Cache GET responses
- Invalidate on updates
- ETags for conditional requests
Summary
What you learned:
- Laravel API development
- API Resources and Collections
- Sanctum authentication
- Error handling patterns
- API testing
Next Steps:
- Read: PHP OOP
- Practice: Add API documentation
- Explore: GraphQL with Laravel
