/ API Integration

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
        ]
    ]);
});