diff --git a/src/Controller/GraphController.php b/src/Controller/GraphController.php index 6d716c9..9861041 100644 --- a/src/Controller/GraphController.php +++ b/src/Controller/GraphController.php @@ -34,8 +34,12 @@ final class GraphController } /** - * Query parameters: - * theme string dark|light (default: dark) + * Serves the contribution heatmap SVG, from a 1-hour cache on repeat requests. + * + * Rejects requests to disallowed hosts (ALLOWED_HOSTS env) with a 403. + * + * @param Request $request query parameter `theme` selects "dark" (default) or "light" + * @return Response image/svg+xml body, cacheable for 3600s */ #[Route('/graph.svg', name: 'contribution_graph', methods: ['GET'])] public function graph(Request $request): Response @@ -66,6 +70,12 @@ final class GraphController ]); } + /** + * Redirects "/" to "/graph.svg", forwarding any query string (e.g. ?theme=light). + * + * @param Request $request incoming request whose query string is preserved + * @return RedirectResponse 302 redirect to /graph.svg + */ #[Route('/', name: 'index', methods: ['GET'])] public function index(Request $request): RedirectResponse { @@ -75,6 +85,12 @@ final class GraphController return new RedirectResponse($url, 302); } + /** + * Reports per-provider connectivity status as JSON. + * + * @return Response 200 with {"status":"ok",...} or 503 with {"status":"degraded",...} + * when any configured provider's probe reports an error + */ #[Route('/health', name: 'health', methods: ['GET'])] public function health(): Response { diff --git a/src/Entity/Contribution.php b/src/Entity/Contribution.php index 2e05227..3472cc6 100644 --- a/src/Entity/Contribution.php +++ b/src/Entity/Contribution.php @@ -4,8 +4,14 @@ declare(strict_types=1); namespace GitContributionGraph\Entity; +/** A single provider's contribution count for one day, as stored in {@see \GitContributionGraph\Service\ContributionStore}. */ final class Contribution { + /** + * @param string $provider provider identifier, e.g. "github" + * @param int $date day of the contribution, as a unix timestamp + * @param int $count contribution count for that day + */ public function __construct( public readonly string $provider, public readonly int $date, diff --git a/src/Service/ContributionStore.php b/src/Service/ContributionStore.php index c793b19..5effcbf 100644 --- a/src/Service/ContributionStore.php +++ b/src/Service/ContributionStore.php @@ -15,6 +15,9 @@ class ContributionStore /** * Opens (creating if needed) the SQLite store at $dbPath and ensures the schema exists. + * + * @param string $dbPath filesystem path to the SQLite database file + * @param ?int $retentionDays days of history to keep; null or 0 keeps rows forever */ public function __construct( private readonly string $dbPath = "%kernel.project_dir%/var/data/contributions.db", @@ -43,6 +46,10 @@ class ContributionStore /** * Inserts a contribution count, overwriting any existing count for the same provider/date. + * + * @param string $provider provider identifier, e.g. "github" + * @param int $unixtime day of the contribution, as a unix timestamp + * @param int $count contribution count for that day */ public function add(string $provider, int $unixtime, int $count): void { @@ -56,6 +63,9 @@ class ContributionStore /** * Deletes the contribution row for the given provider/date, if any. + * + * @param string $provider provider identifier, e.g. "github" + * @param int $unixtime day to delete, as a unix timestamp */ public function remove(string $provider, int $unixtime): void { @@ -69,7 +79,8 @@ class ContributionStore /** * Upserts a batch of date => count pairs for a provider via repeated add() calls. * - * @param array $dateCounts unix timestamp => count + * @param string $provider provider identifier, e.g. "github" + * @param array $dateCounts unix timestamp => count */ public function merge(string $provider, array $dateCounts): void { @@ -80,6 +91,9 @@ class ContributionStore /** * Returns the most recent stored unix timestamp for a provider, or null if it has no rows. + * + * @param string $provider provider identifier, e.g. "github" + * @return ?int unix timestamp of the latest stored day, or null if none stored yet */ public function latestDate(string $provider): ?int { @@ -92,6 +106,10 @@ class ContributionStore /** * Returns all stored contributions for a provider, optionally limited to the last $sinceDays days. + * + * @param string $provider provider identifier, e.g. "github" + * @param ?int $sinceDays if set, only rows from the last N days are returned + * @return ContributionCollection */ public function all(string $provider, ?int $sinceDays = null): ContributionCollection { @@ -106,9 +124,12 @@ class ContributionStore $stmt = $this->pdo->prepare($sql); $stmt->execute($params); + /** @var array $rows */ + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + $contributions = array_map( static fn(array $row): Contribution => new Contribution($row['provider'], (int) $row['date'], (int) $row['count']), - $stmt->fetchAll(PDO::FETCH_ASSOC), + $rows, ); return new ContributionCollection(...$contributions); diff --git a/src/Service/Provider/ProbeTrait.php b/src/Service/Provider/ProbeTrait.php index df3ae4e..194261a 100644 --- a/src/Service/Provider/ProbeTrait.php +++ b/src/Service/Provider/ProbeTrait.php @@ -9,6 +9,10 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; trait ProbeTrait { + /** + * Reports NotConfigured if credentials are missing, otherwise pings the provider + * and reports Ok or Error (with a classified error code and message). + */ public function probe(): ProviderStatus { if (!$this->isConfigured()) { diff --git a/src/Service/Provider/ProviderHealthChecker.php b/src/Service/Provider/ProviderHealthChecker.php index c4a4f85..c81838f 100644 --- a/src/Service/Provider/ProviderHealthChecker.php +++ b/src/Service/Provider/ProviderHealthChecker.php @@ -8,13 +8,17 @@ use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; final class ProviderHealthChecker { + /** @param iterable $providers */ public function __construct( #[AutowireIterator('app.provider')] private readonly iterable $providers, ) {} /** - * @return array{status: string, providers: array>} + * Probes every configured provider and aggregates the results for the /health endpoint. + * + * @return array{status: string, providers: array>} 'status' is + * "degraded" if any provider's probe() reported an error, otherwise "ok" */ public function check(): array { diff --git a/src/Service/Provider/ProviderStatus.php b/src/Service/Provider/ProviderStatus.php index 7ae1517..bfcd0c4 100644 --- a/src/Service/Provider/ProviderStatus.php +++ b/src/Service/Provider/ProviderStatus.php @@ -4,8 +4,15 @@ declare(strict_types=1); namespace GitContributionGraph\Service\Provider; +/** Result of probing a single provider, as reported by the /health endpoint. */ final class ProviderStatus { + /** + * @param string $name provider identifier, e.g. "github" + * @param ProviderStatusType $status overall outcome of the probe + * @param ?ProviderErrorCode $error classified error code, set only when $status is Error + * @param ?string $message human-readable error detail, set only when $status is Error + */ public function __construct( public readonly string $name, public readonly ProviderStatusType $status, @@ -13,7 +20,11 @@ final class ProviderStatus public readonly ?string $message = null, ) {} - /** @return array */ + /** + * Converts to the array shape used in the /health JSON response, omitting error/message when unset. + * + * @return array + */ public function toArray(): array { $data = ['status' => $this->status->value]; diff --git a/src/Service/Renderer/SvgRenderer.php b/src/Service/Renderer/SvgRenderer.php index ab215ce..0c48db2 100644 --- a/src/Service/Renderer/SvgRenderer.php +++ b/src/Service/Renderer/SvgRenderer.php @@ -36,6 +36,12 @@ final class SvgRenderer private const MARGIN_Y = 20; // top margin for month labels private const PADDING = 10; // outer padding + /** + * Builds the full self-contained SVG heatmap for a contribution map. + * + * @param array $contributions date (Y-m-d) => contribution count + * @param string $theme "dark" or "light"; unknown values fall back to "dark" + */ public function render(array $contributions, string $theme = 'dark'): string { $colors = self::THEMES[$theme] ?? self::THEMES['dark']; @@ -91,6 +97,9 @@ final class SvgRenderer /** * Builds a [week][day] grid where each cell is ['date' => 'Y-m-d', 'count' => int] * or null if the date is in the future. + * + * @param array $contributions date (Y-m-d) => contribution count + * @return array> */ private function buildGrid(\DateTimeImmutable $start, \DateTimeImmutable $today, array $contributions): array { @@ -125,6 +134,10 @@ final class SvgRenderer }; } + /** + * @param array $contributions date (Y-m-d) => contribution count + * @return array{total: int} + */ private function computeStats(array $contributions): array { return ['total' => array_sum($contributions)]; @@ -132,6 +145,7 @@ final class SvgRenderer // ------------------------------------------------------------------------- + /** @param array> $grid */ private function renderMonthLabels(array $grid, string $textColor): string { $out = ''; @@ -183,6 +197,10 @@ final class SvgRenderer return $out; } + /** + * @param array> $grid + * @param array{bg: string, text: string, levels: array} $colors + */ private function renderCells(array $grid, array $colors): string { $out = ''; @@ -218,6 +236,7 @@ final class SvgRenderer return $out; } + /** @param array{bg: string, text: string, levels: array} $colors */ private function renderLegend(int $totalH, array $colors): string { $y = $totalH - 14; diff --git a/tests/Unit/Service/Renderer/SvgRendererTest.php b/tests/Unit/Service/Renderer/SvgRendererTest.php index 705bd79..ab8f6cc 100644 --- a/tests/Unit/Service/Renderer/SvgRendererTest.php +++ b/tests/Unit/Service/Renderer/SvgRendererTest.php @@ -54,6 +54,7 @@ final class SvgRendererTest extends TestCase $this->assertStringContainsString($expectedColor, $svg); } + /** @return iterable */ public static function theme_background_provider(): iterable { yield 'dark theme' => ['dark', '#0d1117']; @@ -97,6 +98,7 @@ final class SvgRendererTest extends TestCase $this->assertStringContainsString('>' . $label . '<', $svg); } + /** @return iterable */ public static function day_of_week_label_provider(): iterable { yield 'Monday' => ['Mon'];