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.
107 lines
4.0 KiB
PHP
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'],
|
|
);
|
|
}
|
|
}
|