Files
git-contribution-graph/src/Controller/GraphController.php
T
haylan 9c028aaf5e docs(phpdoc): document public methods across src/
Add professional PHPDoc (summary + @param/@return) to the remaining
public methods that had none or only partial coverage: GraphController's
actions, ContributionStore's accessors, SvgRenderer's render() and
private helpers (with concrete array-shape annotations), ProbeTrait,
ProviderHealthChecker, ProviderStatus, and the Contribution entity.
Also adds the matching @return shapes on SvgRendererTest's data
providers.
2026-07-12 23:43:31 +02:00

107 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace GitContributionGraph\Controller;
use GitContributionGraph\Service\ContributionAggregator;
use GitContributionGraph\Service\Provider\ProviderHealthChecker;
use GitContributionGraph\Service\Renderer\SvgRenderer;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
final class GraphController
{
/** @var list<string> */
private readonly array $allowedHosts;
public function __construct(
private readonly ContributionAggregator $aggregator,
private readonly SvgRenderer $renderer,
private readonly CacheInterface $cache,
private readonly LoggerInterface $logger,
private readonly ProviderHealthChecker $healthChecker,
#[Autowire(env: 'ALLOWED_HOSTS')]
string $allowedHosts = '',
) {
$this->allowedHosts = array_values(array_filter(array_map('trim', explode(',', $allowedHosts))));
}
/**
* 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
{
if ($this->allowedHosts !== [] && !in_array($request->getHost(), $this->allowedHosts, true)) {
$this->logger->warning('GraphController: rejected request from disallowed host', ['host' => $request->getHost()]);
return new Response('Forbidden', 403);
}
$theme = $request->query->get('theme', 'dark');
$cacheKey = 'graph_' . $theme;
$cacheMiss = false;
$svg = $this->cache->get($cacheKey, function (ItemInterface $item) use ($theme, &$cacheMiss): string {
$cacheMiss = true;
$item->expiresAfter(3600);
set_time_limit(30); // ponytail: providers fetch concurrently now (cost ~= max, not sum); GitLab pagination + per-request HTTP timeout (framework.yaml) still need headroom over the 30s default
return $this->renderer->render($this->aggregator->aggregate(), $theme);
});
$this->logger->debug('GraphController: cache ' . ($cacheMiss ? 'miss' : 'hit'), ['theme' => $theme]);
return new Response($svg, 200, [
'Content-Type' => 'image/svg+xml',
'Cache-Control' => 'public, max-age=3600',
]);
}
/**
* 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
{
$query = $request->query->all();
$url = '/graph.svg' . ($query ? '?' . http_build_query($query) : '');
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
{
$result = $this->healthChecker->check();
$statusCode = $result['status'] === 'degraded' ? 503 : 200;
return new Response(
json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
$statusCode,
['Content-Type' => 'application/json'],
);
}
}