-
Notifications
You must be signed in to change notification settings - Fork 0
API Patterns
Rumen Damyanov edited this page Jul 31, 2025
·
1 revision
Complete guide for building RESTful APIs, GraphQL endpoints, and microservices with geographic data using php-geolocation.
- REST API Design
- GraphQL Implementation
- API Authentication
- Rate Limiting
- Response Formatting
- Error Responses
- API Documentation
- Microservices Architecture
- Caching Strategies
- Testing APIs
<?php
// src/Api/GeolocationController.php
namespace Rumenx\Geolocation\Api;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Rumenx\Geolocation\Geolocation;
use Rumenx\Geolocation\Cache\CacheManager;
use Psr\Log\LoggerInterface;
class GeolocationController
{
private Geolocation $geo;
private CacheManager $cache;
private LoggerInterface $logger;
private array $config;
public function __construct(
Geolocation $geo,
CacheManager $cache,
LoggerInterface $logger,
array $config = []
) {
$this->geo = $geo;
$this->cache = $cache;
$this->logger = $logger;
$this->config = array_merge($this->getDefaultConfig(), $config);
}
private function getDefaultConfig(): array
{
return [
'rate_limit' => [
'requests_per_minute' => 60,
'requests_per_hour' => 1000
],
'cache_ttl' => 3600,
'enable_logging' => true,
'allowed_origins' => ['*'],
'api_version' => 'v1'
];
}
/**
* GET /api/v1/geolocation
* Get geolocation data for current IP or specified IP
*/
public function getCurrentLocation(ServerRequestInterface $request): ResponseInterface
{
try {
$queryParams = $request->getQueryParams();
$ip = $queryParams['ip'] ?? $this->getClientIP($request);
$includeDetails = filter_var($queryParams['include_details'] ?? false, FILTER_VALIDATE_BOOLEAN);
// Rate limiting
if (!$this->checkRateLimit($ip)) {
return $this->createErrorResponse(429, 'Rate limit exceeded');
}
// Check cache first
$cacheKey = "geolocation_api_{$ip}_" . ($includeDetails ? 'detailed' : 'basic');
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
$this->logger->info('Geolocation API cache hit', ['ip' => $ip]);
return $this->createSuccessResponse($cached);
}
// Get geolocation data
$this->geo->setIP($ip);
$data = [
'ip' => $ip,
'country' => $this->geo->getCountryCode(),
'country_name' => $this->geo->getCountryName(),
'language' => $this->geo->getLanguage(),
'currency' => $this->geo->getCurrency(),
'timezone' => $this->geo->getTimezone()
];
if ($includeDetails) {
$data = array_merge($data, [
'region' => $this->geo->getRegion(),
'city' => $this->geo->getCity(),
'coordinates' => [
'latitude' => $this->geo->getLatitude(),
'longitude' => $this->geo->getLongitude()
],
'isp' => $this->geo->getISP(),
'organization' => $this->geo->getOrganization(),
'connection_type' => $this->geo->getConnectionType(),
'is_mobile' => $this->geo->isMobile(),
'is_proxy' => $this->geo->isProxy(),
'is_vpn' => $this->geo->isVPN(),
'threat_level' => $this->geo->getThreatLevel()
]);
}
// Add metadata
$response = [
'data' => $data,
'metadata' => [
'timestamp' => time(),
'version' => $this->config['api_version'],
'cached' => false,
'processing_time' => $this->getProcessingTime()
]
];
// Cache the response
$this->cache->set($cacheKey, $response, $this->config['cache_ttl']);
$this->logger->info('Geolocation API request processed', [
'ip' => $ip,
'country' => $data['country'],
'include_details' => $includeDetails
]);
return $this->createSuccessResponse($response);
} catch (\Exception $e) {
$this->logger->error('Geolocation API error', [
'error' => $e->getMessage(),
'ip' => $ip ?? 'unknown'
]);
return $this->createErrorResponse(500, 'Internal server error');
}
}
/**
* GET /api/v1/geolocation/batch
* Get geolocation data for multiple IPs
*/
public function getBatchLocations(ServerRequestInterface $request): ResponseInterface
{
try {
$body = json_decode($request->getBody()->getContents(), true);
$ips = $body['ips'] ?? [];
if (empty($ips) || !is_array($ips)) {
return $this->createErrorResponse(400, 'Invalid or missing IPs array');
}
if (count($ips) > 100) {
return $this->createErrorResponse(400, 'Maximum 100 IPs allowed per batch request');
}
$results = [];
$errors = [];
foreach ($ips as $ip) {
try {
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$errors[] = "Invalid IP: {$ip}";
continue;
}
$this->geo->setIP($ip);
$results[$ip] = [
'country' => $this->geo->getCountryCode(),
'country_name' => $this->geo->getCountryName(),
'language' => $this->geo->getLanguage(),
'currency' => $this->geo->getCurrency(),
'timezone' => $this->geo->getTimezone()
];
} catch (\Exception $e) {
$errors[] = "Error processing IP {$ip}: " . $e->getMessage();
}
}
$response = [
'data' => $results,
'errors' => $errors,
'metadata' => [
'total_requested' => count($ips),
'successful' => count($results),
'failed' => count($errors),
'timestamp' => time(),
'version' => $this->config['api_version']
]
];
return $this->createSuccessResponse($response);
} catch (\Exception $e) {
$this->logger->error('Batch geolocation API error', [
'error' => $e->getMessage()
]);
return $this->createErrorResponse(500, 'Internal server error');
}
}
/**
* GET /api/v1/geolocation/countries
* Get list of supported countries with details
*/
public function getCountries(ServerRequestInterface $request): ResponseInterface
{
try {
$queryParams = $request->getQueryParams();
$language = $queryParams['language'] ?? 'en';
$cacheKey = "countries_list_{$language}";
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $this->createSuccessResponse($cached);
}
$countries = $this->getCountriesData($language);
$response = [
'data' => $countries,
'metadata' => [
'total_countries' => count($countries),
'language' => $language,
'timestamp' => time(),
'version' => $this->config['api_version']
]
];
$this->cache->set($cacheKey, $response, 86400); // Cache for 24 hours
return $this->createSuccessResponse($response);
} catch (\Exception $e) {
$this->logger->error('Countries API error', [
'error' => $e->getMessage()
]);
return $this->createErrorResponse(500, 'Internal server error');
}
}
/**
* GET /api/v1/geolocation/currencies
* Get currency information by country
*/
public function getCurrencies(ServerRequestInterface $request): ResponseInterface
{
try {
$queryParams = $request->getQueryParams();
$country = $queryParams['country'] ?? null;
$cacheKey = $country ? "currency_{$country}" : 'currencies_all';
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $this->createSuccessResponse($cached);
}
if ($country) {
$currency = $this->getCurrencyForCountry($country);
$data = $currency ? [$country => $currency] : [];
} else {
$data = $this->getAllCurrencies();
}
$response = [
'data' => $data,
'metadata' => [
'total_currencies' => count($data),
'country_filter' => $country,
'timestamp' => time(),
'version' => $this->config['api_version']
]
];
$this->cache->set($cacheKey, $response, 86400);
return $this->createSuccessResponse($response);
} catch (\Exception $e) {
$this->logger->error('Currencies API error', [
'error' => $e->getMessage()
]);
return $this->createErrorResponse(500, 'Internal server error');
}
}
/**
* GET /api/v1/geolocation/languages
* Get language information by country
*/
public function getLanguages(ServerRequestInterface $request): ResponseInterface
{
try {
$queryParams = $request->getQueryParams();
$country = $queryParams['country'] ?? null;
$cacheKey = $country ? "languages_{$country}" : 'languages_all';
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $this->createSuccessResponse($cached);
}
if ($country) {
$languages = $this->getLanguagesForCountry($country);
$data = [$country => $languages];
} else {
$data = $this->getAllLanguages();
}
$response = [
'data' => $data,
'metadata' => [
'country_filter' => $country,
'timestamp' => time(),
'version' => $this->config['api_version']
]
];
$this->cache->set($cacheKey, $response, 86400);
return $this->createSuccessResponse($response);
} catch (\Exception $e) {
$this->logger->error('Languages API error', [
'error' => $e->getMessage()
]);
return $this->createErrorResponse(500, 'Internal server error');
}
}
/**
* GET /api/v1/geolocation/timezones
* Get timezone information by country
*/
public function getTimezones(ServerRequestInterface $request): ResponseInterface
{
try {
$queryParams = $request->getQueryParams();
$country = $queryParams['country'] ?? null;
$cacheKey = $country ? "timezones_{$country}" : 'timezones_all';
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $this->createSuccessResponse($cached);
}
if ($country) {
$timezones = $this->getTimezonesForCountry($country);
$data = [$country => $timezones];
} else {
$data = $this->getAllTimezones();
}
$response = [
'data' => $data,
'metadata' => [
'country_filter' => $country,
'timestamp' => time(),
'version' => $this->config['api_version']
]
];
$this->cache->set($cacheKey, $response, 86400);
return $this->createSuccessResponse($response);
} catch (\Exception $e) {
$this->logger->error('Timezones API error', [
'error' => $e->getMessage()
]);
return $this->createErrorResponse(500, 'Internal server error');
}
}
private function createSuccessResponse(array $data, int $status = 200): ResponseInterface
{
$response = new \Nyholm\Psr7\Response($status);
$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withHeader('Access-Control-Allow-Origin', implode(',', $this->config['allowed_origins']));
$response->getBody()->write(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $response;
}
private function createErrorResponse(int $status, string $message, array $details = []): ResponseInterface
{
$error = [
'error' => [
'code' => $status,
'message' => $message,
'details' => $details,
'timestamp' => time(),
'version' => $this->config['api_version']
]
];
$response = new \Nyholm\Psr7\Response($status);
$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withHeader('Access-Control-Allow-Origin', implode(',', $this->config['allowed_origins']));
$response->getBody()->write(json_encode($error, JSON_PRETTY_PRINT));
return $response;
}
private function getClientIP(ServerRequestInterface $request): string
{
$serverParams = $request->getServerParams();
$headers = $request->getHeaders();
// Check various headers for the real IP
$ipHeaders = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_CLIENT_IP'
];
foreach ($ipHeaders as $header) {
if (!empty($serverParams[$header])) {
$ip = trim(explode(',', $serverParams[$header])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $serverParams['REMOTE_ADDR'] ?? '127.0.0.1';
}
private function checkRateLimit(string $ip): bool
{
$minuteKey = "rate_limit_minute_{$ip}_" . floor(time() / 60);
$hourKey = "rate_limit_hour_{$ip}_" . floor(time() / 3600);
$minuteCount = (int) $this->cache->get($minuteKey, 0);
$hourCount = (int) $this->cache->get($hourKey, 0);
if ($minuteCount >= $this->config['rate_limit']['requests_per_minute']) {
return false;
}
if ($hourCount >= $this->config['rate_limit']['requests_per_hour']) {
return false;
}
$this->cache->set($minuteKey, $minuteCount + 1, 60);
$this->cache->set($hourKey, $hourCount + 1, 3600);
return true;
}
private function getProcessingTime(): float
{
return round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2);
}
private function getCountriesData(string $language): array
{
// This would typically come from a database or external service
return [
'US' => [
'code' => 'US',
'name' => $language === 'es' ? 'Estados Unidos' : 'United States',
'currency' => 'USD',
'languages' => ['en', 'es'],
'timezones' => ['America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles']
],
'CA' => [
'code' => 'CA',
'name' => $language === 'fr' ? 'Canada' : 'Canada',
'currency' => 'CAD',
'languages' => ['en', 'fr'],
'timezones' => ['America/St_Johns', 'America/Halifax', 'America/Toronto', 'America/Winnipeg', 'America/Edmonton', 'America/Vancouver']
],
'GB' => [
'code' => 'GB',
'name' => 'United Kingdom',
'currency' => 'GBP',
'languages' => ['en'],
'timezones' => ['Europe/London']
],
'DE' => [
'code' => 'DE',
'name' => $language === 'de' ? 'Deutschland' : 'Germany',
'currency' => 'EUR',
'languages' => ['de'],
'timezones' => ['Europe/Berlin']
],
'FR' => [
'code' => 'FR',
'name' => $language === 'fr' ? 'France' : 'France',
'currency' => 'EUR',
'languages' => ['fr'],
'timezones' => ['Europe/Paris']
]
];
}
private function getCurrencyForCountry(string $country): ?array
{
$currencies = [
'US' => ['code' => 'USD', 'name' => 'US Dollar', 'symbol' => '$'],
'CA' => ['code' => 'CAD', 'name' => 'Canadian Dollar', 'symbol' => 'C$'],
'GB' => ['code' => 'GBP', 'name' => 'British Pound', 'symbol' => '£'],
'DE' => ['code' => 'EUR', 'name' => 'Euro', 'symbol' => '€'],
'FR' => ['code' => 'EUR', 'name' => 'Euro', 'symbol' => '€']
];
return $currencies[$country] ?? null;
}
private function getAllCurrencies(): array
{
return [
'USD' => ['code' => 'USD', 'name' => 'US Dollar', 'symbol' => '$', 'countries' => ['US']],
'CAD' => ['code' => 'CAD', 'name' => 'Canadian Dollar', 'symbol' => 'C$', 'countries' => ['CA']],
'GBP' => ['code' => 'GBP', 'name' => 'British Pound', 'symbol' => '£', 'countries' => ['GB']],
'EUR' => ['code' => 'EUR', 'name' => 'Euro', 'symbol' => '€', 'countries' => ['DE', 'FR', 'ES', 'IT']]
];
}
private function getLanguagesForCountry(string $country): array
{
$languages = [
'US' => [
['code' => 'en', 'name' => 'English', 'native' => 'English', 'primary' => true],
['code' => 'es', 'name' => 'Spanish', 'native' => 'Español', 'primary' => false]
],
'CA' => [
['code' => 'en', 'name' => 'English', 'native' => 'English', 'primary' => true],
['code' => 'fr', 'name' => 'French', 'native' => 'Français', 'primary' => true]
],
'GB' => [
['code' => 'en', 'name' => 'English', 'native' => 'English', 'primary' => true]
],
'DE' => [
['code' => 'de', 'name' => 'German', 'native' => 'Deutsch', 'primary' => true]
],
'FR' => [
['code' => 'fr', 'name' => 'French', 'native' => 'Français', 'primary' => true]
]
];
return $languages[$country] ?? [];
}
private function getAllLanguages(): array
{
return [
'en' => ['code' => 'en', 'name' => 'English', 'native' => 'English', 'countries' => ['US', 'CA', 'GB', 'AU']],
'es' => ['code' => 'es', 'name' => 'Spanish', 'native' => 'Español', 'countries' => ['ES', 'MX', 'AR', 'CO']],
'fr' => ['code' => 'fr', 'name' => 'French', 'native' => 'Français', 'countries' => ['FR', 'CA', 'BE', 'CH']],
'de' => ['code' => 'de', 'name' => 'German', 'native' => 'Deutsch', 'countries' => ['DE', 'AT', 'CH']]
];
}
private function getTimezonesForCountry(string $country): array
{
$timezones = [
'US' => [
'America/New_York' => ['name' => 'Eastern Time', 'offset' => '-05:00'],
'America/Chicago' => ['name' => 'Central Time', 'offset' => '-06:00'],
'America/Denver' => ['name' => 'Mountain Time', 'offset' => '-07:00'],
'America/Los_Angeles' => ['name' => 'Pacific Time', 'offset' => '-08:00']
],
'CA' => [
'America/St_Johns' => ['name' => 'Newfoundland Time', 'offset' => '-03:30'],
'America/Halifax' => ['name' => 'Atlantic Time', 'offset' => '-04:00'],
'America/Toronto' => ['name' => 'Eastern Time', 'offset' => '-05:00'],
'America/Winnipeg' => ['name' => 'Central Time', 'offset' => '-06:00'],
'America/Edmonton' => ['name' => 'Mountain Time', 'offset' => '-07:00'],
'America/Vancouver' => ['name' => 'Pacific Time', 'offset' => '-08:00']
],
'GB' => [
'Europe/London' => ['name' => 'Greenwich Mean Time', 'offset' => '+00:00']
],
'DE' => [
'Europe/Berlin' => ['name' => 'Central European Time', 'offset' => '+01:00']
],
'FR' => [
'Europe/Paris' => ['name' => 'Central European Time', 'offset' => '+01:00']
]
];
return $timezones[$country] ?? [];
}
private function getAllTimezones(): array
{
$allTimezones = [];
$countries = ['US', 'CA', 'GB', 'DE', 'FR'];
foreach ($countries as $country) {
$countryTimezones = $this->getTimezonesForCountry($country);
foreach ($countryTimezones as $tz => $info) {
$allTimezones[$tz] = array_merge($info, ['countries' => [$country]]);
}
}
return $allTimezones;
}
}<?php
// src/GraphQL/GeolocationSchema.php
namespace Rumenx\Geolocation\GraphQL;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Schema;
use Rumenx\Geolocation\Geolocation;
class GeolocationSchema
{
private Geolocation $geo;
public function __construct(Geolocation $geo)
{
$this->geo = $geo;
}
public function build(): Schema
{
return new Schema([
'query' => $this->buildQueryType(),
'mutation' => $this->buildMutationType()
]);
}
private function buildQueryType(): ObjectType
{
return new ObjectType([
'name' => 'Query',
'fields' => [
'geolocation' => [
'type' => $this->buildGeolocationType(),
'args' => [
'ip' => ['type' => Type::string()]
],
'resolve' => function($root, $args) {
$ip = $args['ip'] ?? $this->getCurrentIP();
$this->geo->setIP($ip);
return [
'ip' => $ip,
'country' => $this->geo->getCountryCode(),
'countryName' => $this->geo->getCountryName(),
'region' => $this->geo->getRegion(),
'city' => $this->geo->getCity(),
'language' => $this->geo->getLanguage(),
'currency' => $this->geo->getCurrency(),
'timezone' => $this->geo->getTimezone(),
'coordinates' => [
'latitude' => $this->geo->getLatitude(),
'longitude' => $this->geo->getLongitude()
]
];
}
],
'countries' => [
'type' => Type::listOf($this->buildCountryType()),
'args' => [
'language' => ['type' => Type::string(), 'defaultValue' => 'en']
],
'resolve' => function($root, $args) {
return $this->getCountriesData($args['language']);
}
],
'batchGeolocation' => [
'type' => Type::listOf($this->buildGeolocationType()),
'args' => [
'ips' => ['type' => Type::listOf(Type::string())]
],
'resolve' => function($root, $args) {
$results = [];
foreach ($args['ips'] as $ip) {
try {
$this->geo->setIP($ip);
$results[] = [
'ip' => $ip,
'country' => $this->geo->getCountryCode(),
'countryName' => $this->geo->getCountryName(),
'language' => $this->geo->getLanguage(),
'currency' => $this->geo->getCurrency()
];
} catch (\Exception $e) {
$results[] = [
'ip' => $ip,
'error' => $e->getMessage()
];
}
}
return $results;
}
]
]
]);
}
private function buildMutationType(): ObjectType
{
return new ObjectType([
'name' => 'Mutation',
'fields' => [
'updateGeolocationCache' => [
'type' => Type::boolean(),
'args' => [
'ip' => ['type' => Type::nonNull(Type::string())],
'country' => ['type' => Type::nonNull(Type::string())]
],
'resolve' => function($root, $args) {
// Implementation for cache updates
return true;
}
]
]
]);
}
private function buildGeolocationType(): ObjectType
{
return new ObjectType([
'name' => 'Geolocation',
'fields' => [
'ip' => ['type' => Type::string()],
'country' => ['type' => Type::string()],
'countryName' => ['type' => Type::string()],
'region' => ['type' => Type::string()],
'city' => ['type' => Type::string()],
'language' => ['type' => Type::string()],
'currency' => ['type' => Type::string()],
'timezone' => ['type' => Type::string()],
'coordinates' => ['type' => $this->buildCoordinatesType()],
'error' => ['type' => Type::string()]
]
]);
}
private function buildCoordinatesType(): ObjectType
{
return new ObjectType([
'name' => 'Coordinates',
'fields' => [
'latitude' => ['type' => Type::float()],
'longitude' => ['type' => Type::float()]
]
]);
}
private function buildCountryType(): ObjectType
{
return new ObjectType([
'name' => 'Country',
'fields' => [
'code' => ['type' => Type::string()],
'name' => ['type' => Type::string()],
'currency' => ['type' => Type::string()],
'languages' => ['type' => Type::listOf(Type::string())],
'timezones' => ['type' => Type::listOf(Type::string())]
]
]);
}
private function getCurrentIP(): string
{
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
}
private function getCountriesData(string $language): array
{
// Return countries data - same as REST API
return [
['code' => 'US', 'name' => 'United States', 'currency' => 'USD'],
['code' => 'CA', 'name' => 'Canada', 'currency' => 'CAD'],
['code' => 'GB', 'name' => 'United Kingdom', 'currency' => 'GBP']
];
}
}
// Example GraphQL endpoint
// src/GraphQL/GraphQLEndpoint.php
class GraphQLEndpoint
{
private GeolocationSchema $schema;
public function __construct(GeolocationSchema $schema)
{
$this->schema = $schema;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
try {
$input = json_decode($request->getBody()->getContents(), true);
$query = $input['query'] ?? '';
$variables = $input['variables'] ?? [];
$result = \GraphQL\GraphQL::executeQuery(
$this->schema->build(),
$query,
null,
null,
$variables
);
$response = new \Nyholm\Psr7\Response(200);
$response = $response->withHeader('Content-Type', 'application/json');
$response->getBody()->write(json_encode($result->toArray()));
return $response;
} catch (\Exception $e) {
$error = ['errors' => [['message' => $e->getMessage()]]];
$response = new \Nyholm\Psr7\Response(500);
$response = $response->withHeader('Content-Type', 'application/json');
$response->getBody()->write(json_encode($error));
return $response;
}
}
}<?php
// src/Api/AuthenticationMiddleware.php
namespace Rumenx\Geolocation\Api;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class AuthenticationMiddleware implements MiddlewareInterface
{
private array $config;
private array $publicRoutes;
public function __construct(array $config, array $publicRoutes = [])
{
$this->config = $config;
$this->publicRoutes = $publicRoutes;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$path = $request->getUri()->getPath();
// Check if route is public
if ($this->isPublicRoute($path)) {
return $handler->handle($request);
}
// Check for API key or JWT token
$authResult = $this->authenticate($request);
if (!$authResult['success']) {
return $this->createUnauthorizedResponse($authResult['message']);
}
// Add user context to request
$request = $request->withAttribute('user', $authResult['user']);
$request = $request->withAttribute('api_limits', $authResult['limits']);
return $handler->handle($request);
}
private function authenticate(ServerRequestInterface $request): array
{
// Try API key authentication first
$apiKey = $this->getApiKey($request);
if ($apiKey) {
return $this->authenticateApiKey($apiKey);
}
// Try JWT authentication
$token = $this->getJwtToken($request);
if ($token) {
return $this->authenticateJwt($token);
}
return ['success' => false, 'message' => 'No authentication provided'];
}
private function getApiKey(ServerRequestInterface $request): ?string
{
$headers = $request->getHeaders();
// Check X-API-Key header
if (isset($headers['X-API-Key'][0])) {
return $headers['X-API-Key'][0];
}
// Check query parameter
$queryParams = $request->getQueryParams();
return $queryParams['api_key'] ?? null;
}
private function getJwtToken(ServerRequestInterface $request): ?string
{
$headers = $request->getHeaders();
if (isset($headers['Authorization'][0])) {
$authHeader = $headers['Authorization'][0];
if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
return $matches[1];
}
}
return null;
}
private function authenticateApiKey(string $apiKey): array
{
// In a real implementation, this would check against a database
$validKeys = [
'test_key_123' => [
'user_id' => 'user_1',
'name' => 'Test User',
'limits' => [
'requests_per_minute' => 100,
'requests_per_hour' => 2000,
'features' => ['basic', 'batch']
]
],
'premium_key_456' => [
'user_id' => 'user_2',
'name' => 'Premium User',
'limits' => [
'requests_per_minute' => 500,
'requests_per_hour' => 10000,
'features' => ['basic', 'batch', 'detailed', 'analytics']
]
]
];
if (isset($validKeys[$apiKey])) {
return [
'success' => true,
'user' => $validKeys[$apiKey],
'limits' => $validKeys[$apiKey]['limits']
];
}
return ['success' => false, 'message' => 'Invalid API key'];
}
private function authenticateJwt(string $token): array
{
try {
$decoded = JWT::decode($token, new Key($this->config['jwt_secret'], 'HS256'));
// Verify token claims
if ($decoded->exp < time()) {
return ['success' => false, 'message' => 'Token expired'];
}
return [
'success' => true,
'user' => [
'user_id' => $decoded->sub,
'name' => $decoded->name ?? 'Unknown',
'email' => $decoded->email ?? null
],
'limits' => $decoded->limits ?? [
'requests_per_minute' => 60,
'requests_per_hour' => 1000,
'features' => ['basic']
]
];
} catch (\Exception $e) {
return ['success' => false, 'message' => 'Invalid token: ' . $e->getMessage()];
}
}
private function isPublicRoute(string $path): bool
{
foreach ($this->publicRoutes as $route) {
if (fnmatch($route, $path)) {
return true;
}
}
return false;
}
private function createUnauthorizedResponse(string $message): ResponseInterface
{
$error = [
'error' => [
'code' => 401,
'message' => $message,
'timestamp' => time()
]
];
$response = new \Nyholm\Psr7\Response(401);
$response = $response->withHeader('Content-Type', 'application/json');
$response->getBody()->write(json_encode($error));
return $response;
}
}
// JWT Token Generator
class JwtTokenGenerator
{
private string $secret;
private string $issuer;
public function __construct(string $secret, string $issuer)
{
$this->secret = $secret;
$this->issuer = $issuer;
}
public function generateToken(array $user, int $expiresIn = 3600): string
{
$now = time();
$payload = [
'iss' => $this->issuer,
'sub' => $user['user_id'],
'name' => $user['name'],
'email' => $user['email'] ?? null,
'iat' => $now,
'exp' => $now + $expiresIn,
'limits' => $user['limits'] ?? [
'requests_per_minute' => 60,
'requests_per_hour' => 1000,
'features' => ['basic']
]
];
return JWT::encode($payload, $this->secret, 'HS256');
}
}<?php
// src/Api/RateLimitMiddleware.php
namespace Rumenx\Geolocation\Api;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Rumenx\Geolocation\Cache\CacheManager;
class RateLimitMiddleware implements MiddlewareInterface
{
private CacheManager $cache;
private array $config;
public function __construct(CacheManager $cache, array $config = [])
{
$this->cache = $cache;
$this->config = array_merge($this->getDefaultConfig(), $config);
}
private function getDefaultConfig(): array
{
return [
'global' => [
'requests_per_minute' => 1000,
'requests_per_hour' => 10000
],
'per_ip' => [
'requests_per_minute' => 60,
'requests_per_hour' => 1000
],
'per_user' => [
'requests_per_minute' => 100,
'requests_per_hour' => 2000
],
'endpoints' => [
'/api/v1/geolocation/batch' => [
'requests_per_minute' => 10,
'requests_per_hour' => 100
]
]
];
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$ip = $this->getClientIP($request);
$path = $request->getUri()->getPath();
$user = $request->getAttribute('user');
// Check global rate limits
if (!$this->checkGlobalLimits()) {
return $this->createRateLimitResponse('Global rate limit exceeded');
}
// Check IP-based rate limits
if (!$this->checkIPLimits($ip)) {
return $this->createRateLimitResponse('IP rate limit exceeded');
}
// Check user-based rate limits (if authenticated)
if ($user && !$this->checkUserLimits($user)) {
return $this->createRateLimitResponse('User rate limit exceeded');
}
// Check endpoint-specific rate limits
if (!$this->checkEndpointLimits($ip, $path, $user)) {
return $this->createRateLimitResponse('Endpoint rate limit exceeded');
}
// Add rate limit headers to response
$response = $handler->handle($request);
return $this->addRateLimitHeaders($response, $ip, $user);
}
private function checkGlobalLimits(): bool
{
$minuteKey = 'global_rate_limit_minute_' . floor(time() / 60);
$hourKey = 'global_rate_limit_hour_' . floor(time() / 3600);
$minuteCount = (int) $this->cache->get($minuteKey, 0);
$hourCount = (int) $this->cache->get($hourKey, 0);
if ($minuteCount >= $this->config['global']['requests_per_minute']) {
return false;
}
if ($hourCount >= $this->config['global']['requests_per_hour']) {
return false;
}
$this->cache->set($minuteKey, $minuteCount + 1, 60);
$this->cache->set($hourKey, $hourCount + 1, 3600);
return true;
}
private function checkIPLimits(string $ip): bool
{
$minuteKey = "ip_rate_limit_minute_{$ip}_" . floor(time() / 60);
$hourKey = "ip_rate_limit_hour_{$ip}_" . floor(time() / 3600);
$minuteCount = (int) $this->cache->get($minuteKey, 0);
$hourCount = (int) $this->cache->get($hourKey, 0);
if ($minuteCount >= $this->config['per_ip']['requests_per_minute']) {
return false;
}
if ($hourCount >= $this->config['per_ip']['requests_per_hour']) {
return false;
}
$this->cache->set($minuteKey, $minuteCount + 1, 60);
$this->cache->set($hourKey, $hourCount + 1, 3600);
return true;
}
private function checkUserLimits(array $user): bool
{
$userId = $user['user_id'];
$limits = $user['limits'] ?? $this->config['per_user'];
$minuteKey = "user_rate_limit_minute_{$userId}_" . floor(time() / 60);
$hourKey = "user_rate_limit_hour_{$userId}_" . floor(time() / 3600);
$minuteCount = (int) $this->cache->get($minuteKey, 0);
$hourCount = (int) $this->cache->get($hourKey, 0);
if ($minuteCount >= $limits['requests_per_minute']) {
return false;
}
if ($hourCount >= $limits['requests_per_hour']) {
return false;
}
$this->cache->set($minuteKey, $minuteCount + 1, 60);
$this->cache->set($hourKey, $hourCount + 1, 3600);
return true;
}
private function checkEndpointLimits(string $ip, string $path, ?array $user): bool
{
if (!isset($this->config['endpoints'][$path])) {
return true;
}
$limits = $this->config['endpoints'][$path];
$identifier = $user ? $user['user_id'] : $ip;
$minuteKey = "endpoint_rate_limit_minute_{$path}_{$identifier}_" . floor(time() / 60);
$hourKey = "endpoint_rate_limit_hour_{$path}_{$identifier}_" . floor(time() / 3600);
$minuteCount = (int) $this->cache->get($minuteKey, 0);
$hourCount = (int) $this->cache->get($hourKey, 0);
if ($minuteCount >= $limits['requests_per_minute']) {
return false;
}
if ($hourCount >= $limits['requests_per_hour']) {
return false;
}
$this->cache->set($minuteKey, $minuteCount + 1, 60);
$this->cache->set($hourKey, $hourCount + 1, 3600);
return true;
}
private function addRateLimitHeaders(ResponseInterface $response, string $ip, ?array $user): ResponseInterface
{
$identifier = $user ? $user['user_id'] : $ip;
$limits = $user['limits'] ?? $this->config['per_ip'];
$minuteKey = ($user ? "user" : "ip") . "_rate_limit_minute_{$identifier}_" . floor(time() / 60);
$hourKey = ($user ? "user" : "ip") . "_rate_limit_hour_{$identifier}_" . floor(time() / 3600);
$minuteCount = (int) $this->cache->get($minuteKey, 0);
$hourCount = (int) $this->cache->get($hourKey, 0);
$response = $response->withHeader('X-RateLimit-Limit-Minute', (string) $limits['requests_per_minute']);
$response = $response->withHeader('X-RateLimit-Remaining-Minute', (string) max(0, $limits['requests_per_minute'] - $minuteCount));
$response = $response->withHeader('X-RateLimit-Reset-Minute', (string) ((floor(time() / 60) + 1) * 60));
$response = $response->withHeader('X-RateLimit-Limit-Hour', (string) $limits['requests_per_hour']);
$response = $response->withHeader('X-RateLimit-Remaining-Hour', (string) max(0, $limits['requests_per_hour'] - $hourCount));
$response = $response->withHeader('X-RateLimit-Reset-Hour', (string) ((floor(time() / 3600) + 1) * 3600));
return $response;
}
private function getClientIP(ServerRequestInterface $request): string
{
$serverParams = $request->getServerParams();
$ipHeaders = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_CLIENT_IP'
];
foreach ($ipHeaders as $header) {
if (!empty($serverParams[$header])) {
$ip = trim(explode(',', $serverParams[$header])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $serverParams['REMOTE_ADDR'] ?? '127.0.0.1';
}
private function createRateLimitResponse(string $message): ResponseInterface
{
$error = [
'error' => [
'code' => 429,
'message' => $message,
'timestamp' => time(),
'retry_after' => 60
]
];
$response = new \Nyholm\Psr7\Response(429);
$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withHeader('Retry-After', '60');
$response->getBody()->write(json_encode($error));
return $response;
}
}- 🔧 Configuration Reference - Complete configuration options
⚠️ Error Handling - Comprehensive error handling patterns
Previous: Analytics Integration | Next: Configuration Reference