scanlyser/php-sdk

PHP SDK for the ScanLyser API — accessibility, SEO, performance, UX, and security scanning.

Maintainers

Package info

github.com/scanlyser/php-sdk

Homepage

pkg:composer/scanlyser/php-sdk

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-08 16:27 UTC

This package is auto-updated.

Last update: 2026-07-14 15:52:01 UTC


README

Official PHP SDK for the ScanLyser API. Run accessibility, SEO, performance, UX, and security scans programmatically.

Requirements

  • PHP 8.2+
  • Guzzle 7.0+

Installation

composer require scanlyser/php-sdk

Quick Start

use ScanLyser\Client;
use ScanLyser\Enums\ScanCategory;

$client = new Client(apiKey: 'your-api-token');

// List your sites
$sites = $client->sites($teamId)->list();

foreach ($sites->data as $site) {
    echo "{$site->name}: {$site->url}\n";
}

// Trigger a scoped scan. Omit categories to assess all five public categories.
$scan = $client->scans($teamId)->trigger(
    $siteId,
    wcagLevel: 'AA',
    categories: [ScanCategory::Accessibility, ScanCategory::SEO],
);

// Wait for completion
$scan = $client->scans($teamId)->awaitCompletion($scan->id);

// Get issues
$issues = $client->issues($teamId)->list($scan->id, severity: 'critical');

// Inspect scanner diagnostics independently from findings
$diagnostics = $client->diagnostics($teamId)->list($scan->id);

API Reference

Client

$client = new Client(
    apiKey: 'your-api-token',
    maxRetries: 3, // optional, retries on 429
);

Teams

$teams = $client->teams()->list();
$team = $client->teams()->get($teamId);

Sites

$sites = $client->sites($teamId)->list(perPage: 15);
$site = $client->sites($teamId)->create(name: 'My Site', url: 'https://example.com');
$site = $client->sites($teamId)->get($siteId);
$client->sites($teamId)->delete($siteId);

Scans

use ScanLyser\Enums\ScanCategory;

$scans = $client->scans($teamId)->list($siteId);
$scan = $client->scans($teamId)->trigger(
    $siteId,
    wcagLevel: 'AA',
    webhookUrl: 'https://example.com/webhooks/scanlyser',
    categories: [ScanCategory::SEO, ScanCategory::Performance],
);
$scan = $client->scans($teamId)->get($scanId);

// Poll until complete (default: 600s timeout, 10s interval)
$scan = $client->scans($teamId)->awaitCompletion(
    $scanId,
    timeoutSeconds: 600,
    pollIntervalSeconds: 10,
);

if ($scan->hasUsableScore()) {
    echo "Score: {$scan->scores->overall}\n";
} else {
    echo "Outcome: {$scan->assessmentOutcome?->value}\n";
}

if ($scan->hasUsableCategoryScore(ScanCategory::SEO)) {
    echo "SEO: {$scan->scores->seo}\n";
} else {
    $seoOutcome = $scan->categoryCoverage?->for(ScanCategory::SEO)->outcome->value ?? 'unavailable';
    echo "SEO: {$seoOutcome}\n";
}

Polling stops for completed, failed, and cancelled scans. A completed lifecycle does not by itself guarantee a score: inspect assessmentOutcome, coverage, or use hasUsableScore() before presenting score data. When a scan fails, failure contains only the stable code, customer-safe message, and support correlationId.

The optional categories argument is last in the trigger signature, preserving existing positional calls. Omitting it requests all public categories. Category score members (wcag, seo, performance, ux, sitewide, and other) are nullable: null means that bucket was not scored, never zero. Use requestedCategories, categoryCoverage, scoredCategoryScope, and hasUsableCategoryScore() to distinguish assessed, partial, inconclusive, and not-scanned categories. IssueCategory remains the separate six-value finding/report vocabulary and must not be used to select a scan scope.

Pages

$pages = $client->pages($teamId)->list($scanId);
$page = $client->pages($teamId)->get($scanId, $pageId);

Detailed pages keep issues and diagnostics as separate collections. failure uses the same safe lifecycle-failure object as scans; it never exposes raw exception text or a query-bearing page URL.

Issues

$issues = $client->issues($teamId)->list($scanId);
$issues = $client->issues($teamId)->list($scanId, category: 'wcag', severity: 'critical');

Every issue response is a readonly Finding (which extends the backwards-compatible Issue class) and carries a required FindingResultEnvelope in $issue->result. Issue hydration rejects diagnostic envelopes instead of presenting them as findings. Its nested data objects retain versioned check identity, explicitly nullable qualification, safe reasoning and limitations, structured evidence, reproduction context, remediation parameters, and references.

use ScanLyser\Enums\ResultOutcome;

$result = $issues->data[0]->result;

if ($result->outcome === ResultOutcome::ManualReview) {
    echo $result->explanation->reasoning;
}

Hydration rejects unsupported schema versions and invalid kind/outcome combinations. A null qualification property means the scanner explicitly declined to claim it; do not infer a value from the issue source.

Diagnostics

$diagnostics = $client->diagnostics($teamId)->list($scanId, perPage: 50, page: 2);

foreach ($diagnostics->data as $diagnostic) {
    echo "{$diagnostic->code}: {$diagnostic->detail->message} ({$diagnostic->correlationId})\n";
}

Diagnostic is a separate readonly resource with kind === ResultKind::Diagnostic, an inconclusive or error outcome, string index, check and scope identity, non-null scope link, safe detail, optional recovery action, and support correlation ID. The page argument selects the API page. Diagnostics never contribute to issue counts.

Reports

$report = $client->reports($teamId)->json($scanId);
$client->reports($teamId)->pdf($scanId, saveTo: '/path/to/report.pdf');

Webhook Verification

Verify webhook signatures from scan completion callbacks:

use ScanLyser\Webhooks\WebhookSignature;

$isValid = WebhookSignature::verify(
    payload: $request->getContent(),
    signature: $request->header('X-Signature'),
    secret: $tokenHash,
);

Error Handling

The SDK throws typed exceptions for API errors:

use ScanLyser\Exceptions\AuthenticationException;
use ScanLyser\Exceptions\ForbiddenException;
use ScanLyser\Exceptions\NotFoundException;
use ScanLyser\Exceptions\RateLimitException;
use ScanLyser\Exceptions\ValidationException;

try {
    $site = $client->sites($teamId)->get('nonexistent');
} catch (NotFoundException $exception) {
    // 404
} catch (ValidationException $exception) {
    // 422 - $exception->errors contains field-level errors
} catch (RateLimitException $exception) {
    // 429 - automatic retries exhausted
}

Rate-limited requests (429) are automatically retried up to 3 times with the Retry-After delay.

Laravel Integration

The SDK includes an optional service provider with auto-discovery.

Publish the config:

php artisan vendor:publish --tag=scanlyser-config

Configure .env:

SCANLYSER_TOKEN=your-api-token
SCANLYSER_TEAM=your-team-id

Usage:

use ScanLyser\Client;

class ScanController extends Controller
{
    public function trigger(Client $client): void
    {
        $scan = $client->scans(config('scanlyser.team_id'))
            ->trigger($siteId, wcagLevel: 'AA');
    }
}

License

MIT