Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ All notable changes to this project are documented in this file, following [Keep

### Added

- `Analysis::report()` returns a `Report` value object (scores + Core Web Vitals)
- Support for webmozart/assert 2.x
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,39 @@ $analysis = $pageSpeedApi->analyse('https://example.com/', categories: 'performa
| `locale` | The locale to use for the analysis. | `en` |
| `categories` | The categories to analyze. If not specified, all categories will be analyzed. | - |

## Report

A display-ready view of an analysis: category scores and Core Web Vitals reached by
property, serialisable to array or JSON.

```php
use PageSpeed\Api\PageSpeedApi;

$pageSpeedApi = new PageSpeedApi();
$report = $pageSpeedApi->analyse('https://www.example.com')->report();

// Scores (null when the category was not analysed)
$report->performance->value; // 88
$report->performance->rating; // Rating::NeedsImprovement
$report->performance->rating->value; // 'needs-improvement'
$report->seo->value; // 90

// Core Web Vitals (null when field data is unavailable)
$report->lcp->value; // 2300
$report->lcp->unit; // 'ms'
$report->lcp->rating; // Bucket::Fast

// Serialise for a template, a CLI or an API
$report->toArray();
json_encode($report);
```

Scores (`performance`, `accessibility`, `bestPractices`, `seo`) expose `value` (0-100),
`rating` (a `Rating` following the [Score Evaluation](#score-evaluation) thresholds), and
`category` (`Category`). Metrics (`lcp`, `cls`, `inp`, `fcp`, `fid`, `ttfb`) expose `value`,
`unit`, and `rating` (a `Bucket`: `Fast`, `Average` or `Slow`). Any property is `null` when
the underlying data is absent.

## Audit Scores

![audit-scores.png](docs/audit-scores.png)
Expand Down
6 changes: 6 additions & 0 deletions src/Analysis.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use DateTimeImmutable;
use PageSpeed\Api\Analysis\LighthouseResult;
use PageSpeed\Api\Analysis\LoadingExperience;
use PageSpeed\Api\Report\Report;
use Webmozart\Assert\Assert;

final readonly class Analysis
Expand Down Expand Up @@ -73,6 +74,11 @@ public function getId(): string
return $this->id;
}

public function report(): Report
{
return Report::fromAnalysis($this);
}

/**
* @return array<string, int>
*/
Expand Down
24 changes: 24 additions & 0 deletions src/Report/Bucket.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

/*
* This file is part of the pagespeed/api package.
*
* (c) Simon Andre <smn.andre@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PageSpeed\Api\Report;

/**
* Core Web Vitals rating bucket, mirroring the CrUX metric "category" strings.
*/
enum Bucket: string
{
case Fast = 'FAST';
case Average = 'AVERAGE';
case Slow = 'SLOW';
}
49 changes: 49 additions & 0 deletions src/Report/MetricValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

/*
* This file is part of the pagespeed/api package.
*
* (c) Simon Andre <smn.andre@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PageSpeed\Api\Report;

/**
* A single Core Web Vitals metric with its field value, unit and rating.
*/
final readonly class MetricValue implements \JsonSerializable
{
public function __construct(
public string $name,
public int $value,
public string $unit,
public ?Bucket $rating,
) {
}

/**
* @return array{name: string, value: int, unit: string, rating: string|null}
*/
public function toArray(): array
{
return [
'name' => $this->name,
'value' => $this->value,
'unit' => $this->unit,
'rating' => $this->rating?->value,
];
}

/**
* @return array{name: string, value: int, unit: string, rating: string|null}
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}
33 changes: 33 additions & 0 deletions src/Report/Rating.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/*
* This file is part of the pagespeed/api package.
*
* (c) Simon Andre <smn.andre@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PageSpeed\Api\Report;

/**
* Lighthouse category score rating, following the PageSpeed thresholds.
*/
enum Rating: string
{
case Poor = 'poor';
case NeedsImprovement = 'needs-improvement';
case Good = 'good';

public static function fromScore(int $value): self
{
return match (true) {
$value >= 90 => self::Good,
$value >= 50 => self::NeedsImprovement,
default => self::Poor,
};
}
}
123 changes: 123 additions & 0 deletions src/Report/Report.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

/*
* This file is part of the pagespeed/api package.
*
* (c) Simon Andre <smn.andre@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PageSpeed\Api\Report;

use PageSpeed\Api\Analysis;
use PageSpeed\Api\Analysis\Category;

/**
* Display-ready view over an Analysis: category scores and Core Web Vitals,
* reachable by property and serialisable to array/JSON.
*/
final readonly class Report implements \JsonSerializable
{
/**
* CrUX metric key => [short name, unit].
*
* @var array<string, array{string, string}>
*/
private const array METRICS = [
'LARGEST_CONTENTFUL_PAINT_MS' => ['lcp', 'ms'],
'CUMULATIVE_LAYOUT_SHIFT_SCORE' => ['cls', ''],
'INTERACTION_TO_NEXT_PAINT' => ['inp', 'ms'],
'FIRST_CONTENTFUL_PAINT_MS' => ['fcp', 'ms'],
'FIRST_INPUT_DELAY_MS' => ['fid', 'ms'],
'EXPERIMENTAL_TIME_TO_FIRST_BYTE' => ['ttfb', 'ms'],
];

public function __construct(
public ?Score $performance,
public ?Score $accessibility,
public ?Score $bestPractices,
public ?Score $seo,
public ?MetricValue $lcp,
public ?MetricValue $cls,
public ?MetricValue $inp,
public ?MetricValue $fcp,
public ?MetricValue $fid,
public ?MetricValue $ttfb,
) {
}

public static function fromAnalysis(Analysis $analysis): self
{
$scores = [];
foreach ($analysis->getAuditScores() as $key => $value) {
$category = Category::tryFrom($key);
if (null !== $category) {
$scores[$key] = new Score($category, $value);
}
}

$metrics = [];
foreach ($analysis->loadingExperience->metrics ?? [] as $key => $metric) {
$definition = self::METRICS[$key] ?? null;
if (null === $definition) {
continue;
}
[$name, $unit] = $definition;
$metrics[$name] = new MetricValue($name, $metric->percentile, $unit, Bucket::tryFrom($metric->category));
}

return new self(
performance: $scores['performance'] ?? null,
accessibility: $scores['accessibility'] ?? null,
bestPractices: $scores['best-practices'] ?? null,
seo: $scores['seo'] ?? null,
lcp: $metrics['lcp'] ?? null,
cls: $metrics['cls'] ?? null,
inp: $metrics['inp'] ?? null,
fcp: $metrics['fcp'] ?? null,
fid: $metrics['fid'] ?? null,
ttfb: $metrics['ttfb'] ?? null,
);
}

/**
* @return array{
* scores: array<string, array{category: string, value: int, rating: string}|null>,
* metrics: array<string, array{name: string, value: int, unit: string, rating: string|null}|null>,
* }
*/
public function toArray(): array
{
return [
'scores' => [
'performance' => $this->performance?->toArray(),
'accessibility' => $this->accessibility?->toArray(),
'best-practices' => $this->bestPractices?->toArray(),
'seo' => $this->seo?->toArray(),
],
'metrics' => [
'lcp' => $this->lcp?->toArray(),
'cls' => $this->cls?->toArray(),
'inp' => $this->inp?->toArray(),
'fcp' => $this->fcp?->toArray(),
'fid' => $this->fid?->toArray(),
'ttfb' => $this->ttfb?->toArray(),
],
];
}

/**
* @return array{
* scores: array<string, array{category: string, value: int, rating: string}|null>,
* metrics: array<string, array{name: string, value: int, unit: string, rating: string|null}|null>,
* }
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}
51 changes: 51 additions & 0 deletions src/Report/Score.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

/*
* This file is part of the pagespeed/api package.
*
* (c) Simon Andre <smn.andre@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PageSpeed\Api\Report;

use PageSpeed\Api\Analysis\Category;

/**
* A single Lighthouse category score (0-100) with its rating.
*/
final readonly class Score implements \JsonSerializable
{
public Rating $rating;

public function __construct(
public Category $category,
public int $value,
) {
$this->rating = Rating::fromScore($value);
}

/**
* @return array{category: string, value: int, rating: string}
*/
public function toArray(): array
{
return [
'category' => $this->category->value,
'value' => $this->value,
'rating' => $this->rating->value,
];
}

/**
* @return array{category: string, value: int, rating: string}
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}
13 changes: 13 additions & 0 deletions tests/AnalysisTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,17 @@ public function testGetOriginalLoadingMetricsReturnsCorrectValues(): void
self::assertSame($expectedMetrics, $analysis->getOriginalLoadingMetrics());
}

public function testReport(): void
{
$analysis = Analysis::create(AnalysisFactory::createData([
'lighthouseResult' => ['categories' => [
'performance' => LighthouseCategoryResultFactory::createData(['id' => 'performance', 'score' => 0.9]),
]],
]));

$report = $analysis->report();

self::assertNotNull($report->performance);
self::assertSame(90, $report->performance->value);
}
}
Loading
Loading