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.php

composer.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=info
PHP 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
Documentation - LeakZero | LeakZero