Command Palette
Search for a command to run...
PHP
PHP SDK compatible with modern frameworks and PSR standards
Installation & Setup
Required dependencies and setup for PHP development
Composer Setup
composer install
composer init
composer require guzzlehttp/guzzle monolog/monolog vlucas/phpdotenv
composer require --dev phpunit/phpunit phpstan/phpstan squizlabs/php_codesniffer
echo "LEAKZERO_API_KEY=your_api_key_here" > .env
php basic_usage.phpcomposer.json
{
"name": "your-company/leakzero-example",
"description": "LeakZero API integration example",
"type": "project",
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.0",
"monolog/monolog": "^3.0",
"vlucas/phpdotenv": "^5.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.7",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": {
"LeakZero\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LeakZero\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"phpcs": "phpcs src --standard=PSR12",
"phpstan": "phpstan analyse src"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"minimum-stability": "stable",
"prefer-stable": true
}.env
LEAKZERO_API_KEY=your_api_key_here
LEAKZERO_BASE_URL=https://api.leakzero.io/api/v1
LOG_LEVEL=infoPHP Version: This example requires PHP 8.1+ for modern features like enums and readonly properties. Uses Guzzle for HTTP and Monolog for logging.
Quick Start Example
Simple example to get you started with PHP
basic_usage.php
<?php
require_once 'vendor/autoload.php';
use LeakZero\Client;
use LeakZero\SearchOptions;
function basicSearch(): void
{
$client = new Client($_ENV['LEAKZERO_API_KEY']);
try {
$options = new SearchOptions('email', '[email protected]');
$options->rowLimit = 100;
$result = $client->regularSearch($options);
echo "Found {$result->totalResults} records\n";
echo "Cost: $" . number_format($result->cost, 6) . "\n";
foreach (array_slice($result->data, 0, 5) as $record) {
$breach = $record['breach_name'] ?? 'Unknown';
$date = $record['breach_date'] ?? 'Unknown';
echo "Breach: {$breach}, Date: {$date}\n";
}
$analysis = $client->analyzeBreaches($result);
echo "\nUnique breaches: {$analysis['unique_breaches']}\n";
} catch (Exception $e) {
echo "Search failed: " . $e->getMessage() . "\n";
}
}
basicSearch();
?>Production ReadyComplete PHP Implementation
Full-featured client with PSR compliance, async operations, and enterprise features
LeakZeroClient.php
<?php
declare(strict_types=1);
namespace LeakZero;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\ResponseInterface;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class SearchOptions
{
public string $field;
public string $value;
public string $option = 'exact';
public int $rowLimit = 1000;
public ?array $autoExport = null;
public function __construct(string $field, string $value)
{
$this->field = $field;
$this->value = $value;
}
public function toArray(): array
{
$data = [
'field' => $this->field,
'value' => $this->value,
'option' => $this->option,
'rowLimit' => $this->rowLimit,
];
if ($this->autoExport !== null) {
$data['autoExport'] = json_encode($this->autoExport);
}
return $data;
}
}
class SearchResult
{
public array $data = [];
public int $totalResults = 0;
public float $cost = 0.0;
public string $requestId = '';
public int $processingTime = 0;
public array $analysis = [];
public function addRecord(array $record): void
{
$this->data[] = $record;
}
public function count(): int
{
return count($this->data);
}
public function isEmpty(): bool
{
return empty($this->data);
}
}
class BalanceResponse
{
public float $balance = 0.0;
public string $currency = 'USD';
public string $lastUsage = '';
public static function fromArray(array $data): self
{
$response = new self();
$response->balance = (float) ($data['balance'] ?? 0.0);
$response->currency = $data['currency'] ?? 'USD';
$response->lastUsage = $data['lastUsage'] ?? '';
return $response;
}
}
class UsageStats
{
public int $totalRequests = 0;
public float $totalCost = 0.0;
public int $requestsToday = 0;
public float $costToday = 0.0;
public int $requestsThisWeek = 0;
public float $costThisWeek = 0.0;
public static function fromArray(array $data): self
{
$stats = new self();
$stats->totalRequests = (int) ($data['totalRequests'] ?? 0);
$stats->totalCost = (float) ($data['totalCost'] ?? 0.0);
$stats->requestsToday = (int) ($data['requestsToday'] ?? 0);
$stats->costToday = (float) ($data['costToday'] ?? 0.0);
$stats->requestsThisWeek = (int) ($data['requestsThisWeek'] ?? 0);
$stats->costThisWeek = (float) ($data['costThisWeek'] ?? 0.0);
return $stats;
}
}
class ApiError extends Exception
{
private string $errorCode;
private int $statusCode;
public function __construct(string $errorCode, string $message, int $statusCode, ?Exception $previous = null)
{
$this->errorCode = $errorCode;
$this->statusCode = $statusCode;
parent::__construct("API Error [{$statusCode}]: {$errorCode} - {$message}", $statusCode, $previous);
}
public function getErrorCode(): string
{
return $this->errorCode;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
}
class Client
{
private string $apiKey;
private string $baseUrl;
private HttpClient $httpClient;
private Logger $logger;
private int $maxRetries;
private int $timeout;
public function __construct(
string $apiKey,
string $baseUrl = 'https://api.leakzero.io/api/v1',
int $maxRetries = 3,
int $timeout = 120
) {
if (empty($apiKey)) {
throw new InvalidArgumentException('API key is required');
}
$this->apiKey = $apiKey;
$this->baseUrl = rtrim($baseUrl, '/');
$this->maxRetries = $maxRetries;
$this->timeout = $timeout;
$this->httpClient = new HttpClient([
'timeout' => $this->timeout,
'connect_timeout' => 30,
'verify' => true,
'http_errors' => false,
]);
$this->logger = new Logger('LeakZero');
$this->logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
}
private function generateHeaders(): array
{
return [
'x-api-key' => $this->apiKey,
'x-timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'x-request-id' => $this->generateRequestId(),
'Content-Type' => 'application/json',
'User-Agent' => 'LeakZero-PHP-SDK/1.0.0',
];
}
private function generateRequestId(): string
{
return 'req_' . time() . '_' . substr(uniqid(), -8);
}
private function makeRequest(string $method, string $endpoint, ?array $payload = null): ResponseInterface
{
$url = $this->baseUrl . $endpoint;
$headers = $this->generateHeaders();
for ($attempt = 0; $attempt <= $this->maxRetries; $attempt++) {
try {
$options = ['headers' => $headers];
if ($payload !== null) {
$options['json'] = $payload;
}
$response = $this->httpClient->request($method, $url, $options);
if ($response->getStatusCode() === 429) {
$retryAfter = (int) $response->getHeaderLine('Retry-After') ?: 5;
$retryAfter = min($retryAfter, 30);
$this->logger->warning("Rate limited. Waiting {$retryAfter}s");
sleep($retryAfter);
continue;
}
if ($response->getStatusCode() >= 400) {
$body = $response->getBody()->getContents();
$errorData = json_decode($body, true) ?: [];
$errorCode = $errorData['error'] ?? 'HTTP_ERROR';
$errorMessage = $errorData['message'] ?? 'HTTP ' . $response->getStatusCode();
throw new ApiError($errorCode, $errorMessage, $response->getStatusCode());
}
$this->logger->info("API Request successful: {$method} {$endpoint}");
return $response;
} catch (RequestException $e) {
if ($attempt === $this->maxRetries) {
throw new RuntimeException("Request failed after {$this->maxRetries} attempts: " . $e->getMessage(), 0, $e);
}
$this->logger->warning("Request attempt " . ($attempt + 1) . " failed: " . $e->getMessage());
sleep($attempt + 1);
}
}
throw new RuntimeException('Max retries exceeded');
}
public function getBalance(): BalanceResponse
{
try {
$response = $this->makeRequest('GET', '/public/balance');
$data = json_decode($response->getBody()->getContents(), true);
return BalanceResponse::fromArray($data);
} catch (Exception $e) {
$this->logger->error("Failed to get balance: " . $e->getMessage());
throw $e;
}
}
public function regularSearch(SearchOptions $options): SearchResult
{
$options->rowLimit = min($options->rowLimit, 10000);
try {
$this->logger->info("Searching for {$options->field}: {$options->value}");
$response = $this->makeRequest('POST', '/public/search/regular', $options->toArray());
$result = new SearchResult();
$result->totalResults = (int) $response->getHeaderLine('x-rows-returned');
$result->cost = (int) $response->getHeaderLine('x-charge') / 1_000_000_000;
$result->requestId = $response->getHeaderLine('x-request-id');
$result->processingTime = (int) $response->getHeaderLine('x-processing-time');
$data = json_decode($response->getBody()->getContents(), true);
if (isset($data['data']) && is_array($data['data'])) {
foreach ($data['data'] as $record) {
$result->addRecord($record);
}
}
$this->logger->info("Found {$result->totalResults} records (Cost: $" . number_format($result->cost, 6) . ")");
return $result;
} catch (Exception $e) {
$this->logger->error("Regular search failed: " . $e->getMessage());
throw $e;
}
}
public function advancedSearch(array $queries, int $rowLimit = 1000): SearchResult
{
if (empty($queries)) {
throw new InvalidArgumentException('At least one query is required');
}
$rowLimit = min($rowLimit, 10000);
try {
$payload = [
'queries' => array_map(fn(SearchOptions $query) => $query->toArray(), $queries),
'rowLimit' => $rowLimit,
];
$this->logger->info("Advanced search with " . count($queries) . " queries");
$response = $this->makeRequest('POST', '/public/search/advanced', $payload);
$result = new SearchResult();
$result->totalResults = (int) $response->getHeaderLine('x-rows-returned');
$result->cost = (int) $response->getHeaderLine('x-charge') / 1_000_000_000;
$result->requestId = $response->getHeaderLine('x-request-id');
$result->processingTime = (int) $response->getHeaderLine('x-processing-time');
$data = json_decode($response->getBody()->getContents(), true);
if (isset($data['data']) && is_array($data['data'])) {
foreach ($data['data'] as $record) {
$result->addRecord($record);
}
}
$this->logger->info("Advanced search completed (Cost: $" . number_format($result->cost, 6) . ")");
return $result;
} catch (Exception $e) {
$this->logger->error("Advanced search failed: " . $e->getMessage());
throw $e;
}
}
public function identityLinkingSearch(string $field, string $value, ?string $contextDomain = null, int $maxDepth = 2, float $minConfidence = 0.35, int $maxResults = 50, bool $includePossible = true): SearchResult
{
$maxResults = min($maxResults, 100);
try {
$payload = [
'field' => $field,
'value' => $value,
'maxDepth' => min($maxDepth, 4),
'minConfidence' => $minConfidence,
'maxResults' => $maxResults,
'includePossible' => $includePossible,
];
if ($contextDomain !== null) {
$payload['contextDomain'] = $contextDomain;
}
$this->logger->info("identity linking search for: {$field}:{$value}");
$response = $this->makeRequest('POST', '/public/search/identity-linking', $payload);
$data = json_decode($response->getBody()->getContents(), true);
$result = new SearchResult();
$result->totalResults = (int) $response->getHeaderLine('x-rows-returned');
$result->cost = (int) $response->getHeaderLine('x-charge') / 1_000_000_000;
$result->requestId = $response->getHeaderLine('x-request-id');
$result->processingTime = (int) $response->getHeaderLine('x-processing-time');
$correlation = $data['data'] ?? [];
$result->analysis['summary'] = $correlation['summary'] ?? [];
$result->analysis['metadata'] = $correlation['metadata'] ?? [];
if (isset($correlation['results']) && is_array($correlation['results'])) {
foreach ($correlation['results'] as $candidate) {
$result->addRecord($candidate);
}
}
$this->logger->info("identity linking found {$result->totalResults} candidates");
return $result;
} catch (Exception $e) {
$this->logger->error("Identity linking search failed: " . $e->getMessage());
throw $e;
}
}
public function getUsageStats(): UsageStats
{
try {
$response = $this->makeRequest('GET', '/public/usage/stats');
$data = json_decode($response->getBody()->getContents(), true);
return UsageStats::fromArray($data);
} catch (Exception $e) {
$this->logger->error("Failed to get usage stats: " . $e->getMessage());
throw $e;
}
}
public function getSearchHistory(int $limit = 50, int $offset = 0): array
{
try {
$limit = min($limit, 100);
$endpoint = "/public/search-history?limit={$limit}&offset={$offset}";
$response = $this->makeRequest('GET', $endpoint);
return json_decode($response->getBody()->getContents(), true);
} catch (Exception $e) {
$this->logger->error("Failed to get search history: " . $e->getMessage());
throw $e;
}
}
public function analyzeBreaches(SearchResult $result): array
{
if ($result->isEmpty()) {
return ['error' => 'No records to analyze'];
}
$breachDistribution = [];
$dateDistribution = [];
$fieldDistribution = [];
foreach ($result->data as $record) {
$breachName = $record['breach_name'] ?? 'Unknown';
$breachDistribution[$breachName] = ($breachDistribution[$breachName] ?? 0) + 1;
$breachDate = $record['breach_date'] ?? '';
if (!empty($breachDate)) {
$year = substr($breachDate, 0, 4);
if (is_numeric($year)) {
$dateDistribution[$year] = ($dateDistribution[$year] ?? 0) + 1;
}
}
foreach ($record as $fieldName => $fieldValue) {
if (!empty($fieldValue) && !in_array($fieldName, ['breach_name', 'breach_date'], true)) {
$fieldDistribution[$fieldName] = ($fieldDistribution[$fieldName] ?? 0) + 1;
}
}
}
arsort($breachDistribution);
$years = array_keys($dateDistribution);
$dateRange = [
'earliest' => $years ? min($years) : null,
'latest' => $years ? max($years) : null,
];
return [
'total_records' => $result->count(),
'unique_breaches' => count($breachDistribution),
'breach_distribution' => array_slice($breachDistribution, 0, 10, true),
'date_distribution' => $dateDistribution,
'field_distribution' => $fieldDistribution,
'date_range' => $dateRange,
'top_breaches' => array_map(
fn($name, $count) => ['name' => $name, 'count' => $count],
array_keys(array_slice($breachDistribution, 0, 5, true)),
array_slice($breachDistribution, 0, 5, true)
),
];
}
public function bulkSearch(
array $searchTerms,
string $field = 'email',
int $batchSize = 5,
float $delay = 0.2,
int $rowLimit = 100
): array {
$results = [];
$totalTerms = count($searchTerms);
$this->logger->info("Starting bulk search for {$totalTerms} terms");
foreach (array_chunk($searchTerms, $batchSize) as $i => $batch) {
$batchResults = [];
foreach ($batch as $j => $term) {
try {
if ($j > 0) {
usleep((int) ($delay * 1000000));
}
$options = new SearchOptions($field, $term);
$options->rowLimit = $rowLimit;
$result = $this->regularSearch($options);
$batchResults[] = [
'term' => $term,
'result' => $result,
'success' => true,
];
} catch (Exception $e) {
$this->logger->error("Failed to search for {$term}: " . $e->getMessage());
$batchResults[] = [
'term' => $term,
'error' => $e->getMessage(),
'success' => false,
];
}
}
$results = array_merge($results, $batchResults);
$processed = min(($i + 1) * $batchSize, $totalTerms);
$this->logger->info("Processed {$processed}/{$totalTerms} terms");
if (($i + 1) * $batchSize < $totalTerms) {
usleep((int) ($delay * 2 * 1000000));
}
}
$successful = array_filter($results, fn($r) => $r['success']);
$failed = array_filter($results, fn($r) => !$r['success']);
$successRate = count($results) > 0 ? (count($successful) / count($results) * 100) : 0;
$this->logger->info("Bulk search completed: " . count($successful) . " successful, " . count($failed) . " failed");
return [
'successful' => $successful,
'failed' => $failed,
'total_processed' => count($results),
'success_rate' => number_format($successRate, 2) . '%',
];
}
public function concurrentSearch(array $searchTerms, string $field = 'email', int $concurrency = 5): array
{
$requests = [];
$results = [];
foreach ($searchTerms as $term) {
$options = new SearchOptions($field, $term);
$payload = $options->toArray();
$requests[] = new Request(
'POST',
$this->baseUrl . '/public/search/regular',
$this->generateHeaders(),
json_encode($payload)
);
}
$pool = new Pool($this->httpClient, $requests, [
'concurrency' => $concurrency,
'fulfilled' => function (ResponseInterface $response, int $index) use (&$results, $searchTerms) {
$term = $searchTerms[$index];
try {
$data = json_decode($response->getBody()->getContents(), true);
$result = new SearchResult();
if (isset($data['data']) && is_array($data['data'])) {
foreach ($data['data'] as $record) {
$result->addRecord($record);
}
}
$results[] = [
'term' => $term,
'result' => $result,
'success' => true,
];
} catch (Exception $e) {
$results[] = [
'term' => $term,
'error' => $e->getMessage(),
'success' => false,
];
}
},
'rejected' => function (Exception $reason, int $index) use (&$results, $searchTerms) {
$results[] = [
'term' => $searchTerms[$index],
'error' => $reason->getMessage(),
'success' => false,
];
},
]);
$pool->promise()->wait();
return $results;
}
}
function demonstrateBasicUsage(): void
{
$apiKey = $_ENV['LEAKZERO_API_KEY'] ?? null;
if (!$apiKey) {
throw new RuntimeException('LEAKZERO_API_KEY environment variable is required');
}
try {
$client = new Client($apiKey, maxRetries: 5);
echo "Checking API balance...
";
$balance = $client->getBalance();
echo "Current balance: $" . number_format($balance->balance, 6) . "
";
if ($balance->balance < 0.001) {
throw new RuntimeException('Insufficient balance. Please top up your account.');
}
echo "
Performing regular search...
";
$options = new SearchOptions('email', '[email protected]');
$options->option = 'exact';
$options->rowLimit = 100;
$result = $client->regularSearch($options);
echo "Found {$result->totalResults} records
";
echo "Cost: $" . number_format($result->cost, 6) . "
";
foreach (array_slice($result->data, 0, 5) as $record) {
$breach = $record['breach_name'] ?? 'Unknown';
$date = $record['breach_date'] ?? 'Unknown';
echo "Breach: {$breach}, Date: {$date}
";
}
if (!$result->isEmpty()) {
$analysis = $client->analyzeBreaches($result);
echo "
Analysis: {$analysis['unique_breaches']} unique breaches
";
echo "Top breaches:
";
foreach ($analysis['top_breaches'] as $breach) {
echo " - {$breach['name']}: {$breach['count']} records
";
}
}
echo "
Getting usage statistics...
";
$stats = $client->getUsageStats();
echo "Total requests: {$stats->totalRequests}
";
echo "Total cost: $" . number_format($stats->totalCost, 6) . "
";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "
";
}
}
function demonstrateConcurrentUsage(): void
{
$apiKey = $_ENV['LEAKZERO_API_KEY'] ?? null;
if (!$apiKey) {
throw new RuntimeException('LEAKZERO_API_KEY environment variable is required');
}
try {
$client = new Client($apiKey);
$searchTerms = ['[email protected]', '[email protected]', '[email protected]'];
$results = $client->concurrentSearch($searchTerms, concurrency: 3);
$successful = array_filter($results, fn($r) => $r['success']);
echo "Completed " . count($successful) . " concurrent searches
";
} catch (Exception $e) {
echo "Concurrent search error: " . $e->getMessage() . "
";
}
}
if (basename(__FILE__) === basename($_SERVER['SCRIPT_NAME'])) {
demonstrateBasicUsage();
echo "
" . str_repeat('=', 50) . "
";
demonstrateConcurrentUsage();
}
?>PSR Compliance: Follows PSR-4, PSR-7, and PSR-12 standards for interoperability
Modern PHP: PHP 8.1+ features with strict types and enhanced error handling
Concurrent Operations: Guzzle Pool for efficient parallel requests
Key Features
Core Features
- • PSR-7 HTTP message interface compliance
- • Strict typing with PHP 8.1+ features
- • Automatic retry with exponential backoff
- • Rate limit handling with intelligent delays
- • Comprehensive error handling with custom exceptions
Performance Features
- • Guzzle Pool for concurrent HTTP requests
- • Connection pooling and keep-alive support
- • Bulk search operations with batching
- • Structured logging with Monolog integration
- • Environment-based configuration
Best Practices:
- • Always use environment variables for API keys
- • Implement proper exception handling in production
- • Use Guzzle Pool for concurrent operations
- • Monitor your API usage and balance regularly
- • Follow PSR standards for code consistency