A simple and elegant time synchronization library for PHP that allows you to synchronize your application's clock with a remote time source based on IP geolocation.
- 🕐 PSR-20 Clock Interface - Fully compliant with the PSR-20 Clock standard
- 🌍 IP Geolocation - Fetch time data for an IPv4 address via ipgeolocation.io
- 🔍 Server IP Detection - Automatically detect the current server IP via
SyncService - ⚡ Microsecond Precision - Maintains microsecond-level time accuracy
- 🔌 PSR-18 HTTP Client - Works with any PSR-18 compatible HTTP client
- 🧪 Fully Tested - Covered by PHPUnit tests
- 🎯 Modern PHP - Requires PHP 8.4+
Install via Composer:
composer require hexogen/timesync- PHP 8.4 or higher
- A PSR-18 compatible HTTP client (for example Guzzle or Symfony HttpClient)
Version 0.3.0 contains a breaking API change:
IPGeolocationClient,SyncService, andIpifyIPDetectornow expose dedicated exception classes as part of the public contract- Catch
InvalidIpAddressException,GeolocationServiceException, andServerIpDetectionExceptionfor precise failure handling - All library-specific exceptions now implement
TimesyncException, so you can catch a single domain-level type when preferred 0.2.xalready introduced theSyncServicesplit for automatic server IP detection; that API remains the current usage model
use Psr\Http\Client\ClientExceptionInterface;
try {
$clock = $syncService->getCurrentTime();
} catch (\InvalidArgumentException $e) {
// Invalid IPv4 address format
} catch (\RuntimeException $e) {
// API error, malformed response, or detector failure
} catch (ClientExceptionInterface $e) {
// HTTP client error
}use Hexogen\Timesync\GeolocationServiceException;
use Hexogen\Timesync\InvalidIpAddressException;
use Hexogen\Timesync\ServerIpDetectionException;
use Psr\Http\Client\ClientExceptionInterface;
try {
$clock = $syncService->getCurrentTime();
} catch (InvalidIpAddressException $e) {
// Invalid IPv4 address format
} catch (GeolocationServiceException $e) {
// ipgeolocation.io returned an error or malformed payload
} catch (ServerIpDetectionException $e) {
// The current server IP could not be determined
} catch (ClientExceptionInterface $e) {
// HTTP client transport error
}You can also catch Hexogen\Timesync\TimesyncException to handle all library-specific failures with one catch block.
<?php
use GuzzleHttp\Client;
use Hexogen\Timesync\IPGeolocationClient;
use Hexogen\Timesync\IpifyIPDetector;
use Hexogen\Timesync\SyncService;
$httpClient = new Client();
$ipDetector = new IpifyIPDetector($httpClient);
$syncClient = new IPGeolocationClient(
apiKey: 'your-ipgeolocation-api-key',
httpClient: $httpClient,
);
$syncService = new SyncService($syncClient, $ipDetector);
$clock = $syncService->getCurrentTime();
echo $clock->now()->format('Y-m-d H:i:s.u');The Clock class provides a synchronized time based on a reference timestamp:
use Hexogen\Timesync\Clock;
$referenceTime = microtime(true) + 5.0;
$clock = new Clock($referenceTime, 'Europe/Kyiv');
$now = $clock->now();Use IPGeolocationClient when you already know which IPv4 address you want to resolve:
use GuzzleHttp\Client;
use Hexogen\Timesync\IPGeolocationClient;
$httpClient = new Client();
$client = new IPGeolocationClient('your-api-key', $httpClient);
$clock = $client->getCurrentTime('8.8.8.8');
echo $clock->now()->getTimezone()->getName();Use SyncService when you want the library to detect the current server IP for you:
use GuzzleHttp\Client;
use Hexogen\Timesync\IPGeolocationClient;
use Hexogen\Timesync\IpifyIPDetector;
use Hexogen\Timesync\SyncService;
$httpClient = new Client();
$ipDetector = new IpifyIPDetector($httpClient);
$syncClient = new IPGeolocationClient('your-api-key', $httpClient);
$syncService = new SyncService($syncClient, $ipDetector);
$clock = $syncService->getCurrentTime();You can still override the detected IP explicitly:
$clock = $syncService->getCurrentTime('37.17.245.123');Implement your own IP detector:
use Hexogen\Timesync\ServerIPDetectorInterface;
class CustomIPDetector implements ServerIPDetectorInterface
{
public function getCurrentServerIP(): string
{
return $this->someService->fetchServerIP();
}
}Implements Psr\Clock\ClockInterface
public function __construct(
float $realTimestamp,
string $timeZone = 'UTC'
)
public function now(): DateTimeImmutableImplements SyncClientInterface
public function __construct(
string $apiKey,
ClientInterface $httpClient
)
public function getCurrentTime(string $ip): ClockInterfaceThrows InvalidIpAddressException, GeolocationServiceException, and ClientExceptionInterface.
Implements SyncServiceInterface
public function __construct(
SyncClientInterface $syncClient,
ServerIPDetectorInterface $serverIPDetector
)
public function getCurrentTime(?string $ip = null): ClockInterfaceThrows InvalidIpAddressException, GeolocationServiceException, ServerIpDetectionException, and ClientExceptionInterface.
Implements ServerIPDetectorInterface
public function __construct(ClientInterface $httpClient)
public function getCurrentServerIP(): stringThrows ServerIpDetectionException and ClientExceptionInterface.
TimesyncException— marker interface implemented by all library-specific exceptionsInvalidIpAddressException— invalid IPv4 input passed to the libraryGeolocationServiceException— geolocation API returned an error or malformed payloadServerIpDetectionException— public server IP lookup failed or returned an invalid payload
- Sign up at ipgeolocation.io
- Get your API key from the dashboard
- Pass it to
IPGeolocationClient
The library automatically uses the timezone returned by the geolocation API. You can also manually specify a timezone when creating a Clock:
$clock = new Clock(microtime(true), 'America/New_York');Run the test suite:
composer testRun tests with coverage:
./vendor/bin/phpunit tests/ --coverage-html coverageCheck code style:
composer lintFix code style:
composer fixThis library follows:
- PER-CS (PHP Evolving Recommendations for Code Style)
- PSR-12 Extended Coding Style Guide
- Strict types declaration in all files
- Strong typing with explicit interfaces and return types
┌─────────────────────────────────────┐
│ SyncService │
│ (SyncServiceInterface) │
└───────────┬─────────────────────────┘
│
├──► IpifyIPDetector
│ (ServerIPDetectorInterface)
| └──► PSR-18 HTTP Client
│
└──► IPGeolocationClient
(SyncClientInterface)
│
├──► PSR-18 HTTP Client
│
└──► Clock (PSR-20)
└──► DateTimeImmutable
- Optional IP Detection:
SyncServicefetches the current server IP when you do not provide one - Geolocation:
IPGeolocationClientqueries the ipgeolocation.io API with an IPv4 address - Time Extraction: The API returns the current time and timezone for that IP's location
- Clock Creation: A
Clockinstance stores the time delta and timezone - Synchronized Time: The clock provides synchronized time adjusted for the delta
All library-specific exceptions implement Hexogen\Timesync\TimesyncException.
use Hexogen\Timesync\GeolocationServiceException;
use Hexogen\Timesync\InvalidIpAddressException;
use Hexogen\Timesync\ServerIpDetectionException;
use Hexogen\Timesync\TimesyncException;
use Psr\Http\Client\ClientExceptionInterface;
try {
$clock = $syncService->getCurrentTime();
} catch (InvalidIpAddressException $e) {
// Invalid IPv4 address format supplied to the library
echo $e->getMessage();
} catch (GeolocationServiceException $e) {
// ipgeolocation.io returned an error or unexpected payload
echo $e->getMessage();
} catch (ServerIpDetectionException $e) {
// The current server IP could not be determined
echo $e->getMessage();
} catch (ClientExceptionInterface $e) {
// HTTP client transport error
echo $e->getMessage();
} catch (TimesyncException $e) {
// Optional single catch for any other library-specific exception type
echo $e->getMessage();
}- Delta calculation: The clock calculates the time offset once per instance
- Lightweight: Minimal overhead, no background processes
- Composable: Cache
ClockorSyncServiceresults in your DI container if needed
// In a service provider
$this->app->singleton(ClockInterface::class, function () {
$httpClient = new \GuzzleHttp\Client();
$ipDetector = new IpifyIPDetector($httpClient);
$syncClient = new IPGeolocationClient(
config('services.ipgeolocation.key'),
$httpClient,
);
$syncService = new SyncService($syncClient, $ipDetector);
return $syncService->getCurrentTime();
});# config/services.yaml
services:
Hexogen\Timesync\IpifyIPDetector:
arguments:
- '@Psr\Http\Client\ClientInterface'
Hexogen\Timesync\IPGeolocationClient:
arguments:
- '%env(IPGEOLOCATION_API_KEY)%'
- '@Psr\Http\Client\ClientInterface'
Hexogen\Timesync\SyncService:
arguments:
- '@Hexogen\Timesync\IPGeolocationClient'
- '@Hexogen\Timesync\IpifyIPDetector'Contributions are welcome. Feel free to submit a Pull Request.
# Clone the repository
git clone https://github.com/hexogen/timesync.git
cd timesync
# Install dependencies
composer install
# Run tests
composer test
# Check code style
composer lint
# Fix code style
composer fixThis library is licensed under the MIT License. See LICENSE for details.
- Developed by Hexogen
- Uses ipgeolocation.io for time synchronization
- Uses ipify.org for server IP detection
- Breaking: introduced dedicated domain exceptions as part of the public API:
InvalidIpAddressException,GeolocationServiceException, andServerIpDetectionException - Breaking: added
TimesyncExceptionas a marker interface for all library-specific exceptions - Updated interface contracts and README examples to document the new exception handling model
- Corrected the README PHP requirement to match
composer.json(PHP 8.3+)
- Breaking: moved optional server IP detection from
IPGeolocationClienttoSyncService - Breaking:
IPGeolocationClient::__construct()now accepts onlyapiKeyandhttpClient - Breaking:
IPGeolocationClient::getCurrentTime()now requires an explicit IPv4 string - Updated documentation and examples to reflect the new API split
- Added MIT
LICENSEfile - Improved workflow with coverage reporting
- README fixes and consistency improvements
- Initial release
- PSR-20 Clock implementation
- IP geolocation time synchronization
- Microsecond precision support
- Full test coverage
- Issues: GitHub Issues
- Documentation: GitHub Wiki
- Discussions: GitHub Discussions