API Integration Examples
Complete examples of building REST APIs with Miko ORM, HttpClient, and JWT authentication.
REST API Structure
api/
├── index.php # Entry point & router
├── controllers/
│ ├── AuthController.php
│ ├── UserController.php
│ └── ProductController.php
├── middleware/
│ └── AuthMiddleware.php
└── .htaccess # URL rewriting
API Entry Point
index.php
<?php
require_once '../Model/Miko/autoload.php';
use Miko\Core\Http\{Cors, JsonResponse};
// Handle CORS
Cors::handle([
'origins' => ['https://myapp.com'],
'methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'headers' => ['Content-Type', 'Authorization'],
'credentials' => true
]);
// Parse request
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = str_replace('/api', '', $uri);
// Simple router
try {
route($method, $uri);
} catch (Exception $e) {
JsonResponse::error($e->getMessage(), 500);
}
function route(string $method, string $uri): void
{
// Auth routes (no authentication required)
if ($uri === '/auth/login' && $method === 'POST') {
(new AuthController())->login();
return;
}
if ($uri === '/auth/register' && $method === 'POST') {
(new AuthController())->register();
return;
}
if ($uri === '/auth/refresh' && $method === 'POST') {
(new AuthController())->refresh();
return;
}
// Protected routes - require authentication
$user = AuthMiddleware::authenticate();
// User routes
if (preg_match('/^\/users$/', $uri)) {
$controller = new UserController();
match($method) {
'GET' => $controller->index(),
'POST' => $controller->store(),
default => JsonResponse::error('Method not allowed', 405)
};
return;
}
if (preg_match('/^\/users\/(\d+)$/', $uri, $matches)) {
$controller = new UserController();
$id = (int)$matches[1];
match($method) {
'GET' => $controller->show($id),
'PUT' => $controller->update($id),
'DELETE' => $controller->destroy($id),
default => JsonResponse::error('Method not allowed', 405)
};
return;
}
// Product routes
if (preg_match('/^\/products$/', $uri)) {
$controller = new ProductController();
match($method) {
'GET' => $controller->index(),
'POST' => $controller->store(),
default => JsonResponse::error('Method not allowed', 405)
};
return;
}
JsonResponse::notFound('Endpoint not found');
}
Authentication
AuthMiddleware.php
<?php
use Miko\Core\Http\{JwtHelper, JsonResponse};
class AuthMiddleware
{
private static ?JwtHelper $jwt = null;
private static function getJwt(): JwtHelper
{
if (self::$jwt === null) {
self::$jwt = new JwtHelper(
$_ENV['JWT_SECRET'],
$_ENV['APP_NAME'],
'api',
60
);
}
return self::$jwt;
}
public static function authenticate(): array
{
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/Bearer\s+(.+)/', $header, $matches)) {
JsonResponse::unauthorized('Token required');
exit;
}
$result = self::getJwt()->validateToken($matches[1]);
if (!$result->isValid) {
JsonResponse::unauthorized($result->error);
exit;
}
return [
'user_id' => $result->getUserId(),
'email' => $result->getEmail(),
'role' => $result->getRole()
];
}
public static function requireRole(string ...$roles): array
{
$user = self::authenticate();
if (!in_array($user['role'], $roles)) {
JsonResponse::forbidden('Insufficient permissions');
exit;
}
return $user;
}
}
AuthController.php
<?php
use Miko\Core\Http\{JwtHelper, JsonResponse};
use Miko\Library\Crypto;
class AuthController
{
private JwtHelper $jwt;
private JwtHelper $refreshJwt;
public function __construct()
{
$this->jwt = new JwtHelper(
$_ENV['JWT_SECRET'],
$_ENV['APP_NAME'],
'api',
60 // 1 hour
);
$this->refreshJwt = new JwtHelper(
$_ENV['JWT_REFRESH_SECRET'],
$_ENV['APP_NAME'],
'api',
10080 // 7 days
);
}
public function register(): void
{
$data = $this->getJsonInput();
// Validate
$errors = $this->validateRegistration($data);
if (!empty($errors)) {
JsonResponse::validationError($errors);
return;
}
// Check if email exists
if (User::where('Email', $data['email'])->exists()) {
JsonResponse::validationError(['email' => 'Email already registered']);
return;
}
// Create user
$user = User::create([
'Name' => $data['name'],
'Email' => $data['email'],
'Password' => Crypto::hashPassword($data['password']),
'Role' => 'user'
]);
// Generate tokens
$tokens = $this->generateTokens($user);
JsonResponse::created([
'user' => $user->only('Id', 'Name', 'Email', 'Role'),
'tokens' => $tokens
]);
}
public function login(): void
{
$data = $this->getJsonInput();
// Find user
$user = User::where('Email', $data['email'] ?? '')->first();
if (!$user || !Crypto::verifyPassword($data['password'] ?? '', $user->Password)) {
JsonResponse::unauthorized('Invalid credentials');
return;
}
// Check if active
if (!$user->IsActive) {
JsonResponse::forbidden('Account is disabled');
return;
}
// Generate tokens
$tokens = $this->generateTokens($user);
// Update last login
$user->LastLoginAt = date('Y-m-d H:i:s');
$user->save();
JsonResponse::success([
'user' => $user->only('Id', 'Name', 'Email', 'Role'),
'tokens' => $tokens
]);
}
public function refresh(): void
{
$data = $this->getJsonInput();
$refreshToken = $data['refresh_token'] ?? '';
$result = $this->refreshJwt->validateToken($refreshToken);
if (!$result->isValid) {
JsonResponse::unauthorized('Invalid refresh token');
return;
}
$user = User::find($result->getUserId());
if (!$user || !$user->IsActive) {
JsonResponse::unauthorized('User not found or disabled');
return;
}
// Generate new access token
$accessToken = $this->jwt->generateUserToken(
$user->Id,
$user->Email,
$user->Role
);
JsonResponse::success([
'access_token' => $accessToken,
'expires_in' => 3600
]);
}
private function generateTokens(User $user): array
{
return [
'access_token' => $this->jwt->generateUserToken(
$user->Id,
$user->Email,
$user->Role
),
'refresh_token' => $this->refreshJwt->generate([
'user_id' => $user->Id
]),
'expires_in' => 3600
];
}
private function validateRegistration(array $data): array
{
$errors = [];
if (empty($data['name'])) {
$errors['name'] = 'Name is required';
}
if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Valid email is required';
}
if (empty($data['password']) || strlen($data['password']) < 8) {
$errors['password'] = 'Password must be at least 8 characters';
}
return $errors;
}
private function getJsonInput(): array
{
return json_decode(file_get_contents('php://input'), true) ?? [];
}
}
Resource Controllers
UserController.php
<?php
use Miko\Core\Http\JsonResponse;
class UserController
{
public function index(): void
{
$page = (int)($_GET['page'] ?? 1);
$perPage = (int)($_GET['per_page'] ?? 20);
$search = $_GET['search'] ?? '';
$query = User::query()->where('IsActive', true);
if ($search) {
$query->whereLike('Name', $search)
->orWhereLike('Email', $search);
}
$result = $query->orderBy('Name')->paginate($perPage, $page);
JsonResponse::paginated(
array_map(fn($u) => $u->only('Id', 'Name', 'Email', 'Role', 'CreatedDate'), $result['data']),
[
'current_page' => $result['current_page'],
'last_page' => $result['last_page'],
'total' => $result['total'],
'per_page' => $result['per_page'],
]
);
}
public function show(int $id): void
{
$user = User::with('profile')->find($id);
if (!$user) {
JsonResponse::notFound('User not found');
return;
}
JsonResponse::success([
'user' => [
'id' => $user->Id,
'name' => $user->Name,
'email' => $user->Email,
'role' => $user->Role,
'profile' => $user->profile ? [
'bio' => $user->profile->Bio,
'avatar' => $user->profile->Avatar
] : null,
'created_at' => $user->CreatedDate
]
]);
}
public function store(): void
{
// Require admin role
AuthMiddleware::requireRole('admin');
$data = json_decode(file_get_contents('php://input'), true);
// Validate
if (empty($data['name']) || empty($data['email'])) {
JsonResponse::validationError([
'name' => 'Name is required',
'email' => 'Email is required'
]);
return;
}
$user = User::create([
'Name' => $data['name'],
'Email' => $data['email'],
'Password' => Crypto::hashPassword($data['password'] ?? 'changeme'),
'Role' => $data['role'] ?? 'user'
]);
JsonResponse::created($user->only('Id', 'Name', 'Email', 'Role'));
}
public function update(int $id): void
{
$currentUser = AuthMiddleware::authenticate();
// Users can update themselves, admins can update anyone
if ($currentUser['user_id'] !== $id && $currentUser['role'] !== 'admin') {
JsonResponse::forbidden('Cannot update other users');
return;
}
$user = User::find($id);
if (!$user) {
JsonResponse::notFound('User not found');
return;
}
$data = json_decode(file_get_contents('php://input'), true);
// Update allowed fields
if (isset($data['name'])) $user->Name = $data['name'];
if (isset($data['email'])) $user->Email = $data['email'];
// Only admin can change role
if (isset($data['role']) && $currentUser['role'] === 'admin') {
$user->Role = $data['role'];
}
$user->save();
JsonResponse::success($user->only('Id', 'Name', 'Email', 'Role'));
}
public function destroy(int $id): void
{
AuthMiddleware::requireRole('admin');
$user = User::find($id);
if (!$user) {
JsonResponse::notFound('User not found');
return;
}
$user->delete();
JsonResponse::noContent();
}
}
Consuming External APIs
External API Client
<?php
use Miko\Core\Http\HttpClient;
class PaymentGateway
{
private HttpClient $client;
public function __construct()
{
$this->client = HttpClient::create($_ENV['PAYMENT_API_URL']);
$this->client->setBearerToken($_ENV['PAYMENT_API_KEY']);
$this->client->setTimeout(30);
}
public function createCharge(array $data): array
{
$response = $this->client->post('/charges', [
'amount' => $data['amount'],
'currency' => $data['currency'] ?? 'USD',
'source' => $data['token'],
'description' => $data['description'] ?? ''
]);
if (!$response->ok()) {
throw new Exception('Payment failed: ' . $response->json()['error'] ?? 'Unknown error');
}
return $response->json();
}
public function refund(string $chargeId, ?float $amount = null): array
{
$data = ['charge' => $chargeId];
if ($amount) {
$data['amount'] = $amount;
}
$response = $this->client->post('/refunds', $data);
$response->throwIfFailed();
return $response->json();
}
public function getCharge(string $chargeId): ?array
{
$response = $this->client->get("/charges/{$chargeId}");
if ($response->status() === 404) {
return null;
}
$response->throwIfFailed();
return $response->json();
}
}
// Usage
$gateway = new PaymentGateway();
try {
$charge = $gateway->createCharge([
'amount' => 9999, // $99.99 in cents
'token' => 'tok_visa',
'description' => 'Order #123'
]);
echo "Charge ID: " . $charge['id'];
} catch (Exception $e) {
echo "Payment failed: " . $e->getMessage();
}
Webhook Handler
<?php
use Miko\Core\Http\JsonResponse;
use Miko\Library\Crypto;
use Miko\Log\Logger;
class WebhookController
{
public function handlePayment(): void
{
// Verify signature
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expectedSignature = Crypto::hmac($payload, $_ENV['WEBHOOK_SECRET'], 'sha256');
if (!hash_equals($expectedSignature, $signature)) {
JsonResponse::unauthorized('Invalid signature');
return;
}
$event = json_decode($payload, true);
// Handle event
switch ($event['type']) {
case 'charge.succeeded':
$this->handleChargeSucceeded($event['data']);
break;
case 'charge.failed':
$this->handleChargeFailed($event['data']);
break;
case 'refund.created':
$this->handleRefundCreated($event['data']);
break;
default:
// Unknown event type - log and ignore
Logger::api('Unknown webhook event: ' . $event['type'], [], 'INFO');
}
JsonResponse::success(['received' => true]);
}
private function handleChargeSucceeded(array $data): void
{
$order = Order::where('PaymentId', $data['id'])->first();
if ($order) {
$order->Status = 'paid';
$order->PaidAt = date('Y-m-d H:i:s');
$order->save();
// Send confirmation email
EmailService::sendOrderConfirmation($order);
}
}
private function handleChargeFailed(array $data): void
{
$order = Order::where('PaymentId', $data['id'])->first();
if ($order) {
$order->Status = 'payment_failed';
$order->save();
// Notify customer
EmailService::sendPaymentFailed($order);
}
}
private function handleRefundCreated(array $data): void
{
$order = Order::where('PaymentId', $data['charge'])->first();
if ($order) {
$order->Status = 'refunded';
$order->RefundedAt = date('Y-m-d H:i:s');
$order->save();
}
}
}
Error Handling
Global Exception Handler
<?php
set_exception_handler(function(Throwable $e) {
$code = $e->getCode() ?: 500;
if ($code < 100 || $code > 599) {
$code = 500;
}
// Log error
Logger::error($e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
// Return JSON error
http_response_code($code);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => [
'message' => $e->getMessage(),
'code' => $code
]
]);
});