5 Commits
10 changed files with 374 additions and 1539 deletions
+8 -6
View File
@@ -6,17 +6,19 @@ Order matters — later steps assume earlier ones are done.
## 1. Audit cleanup ## 1. Audit cleanup
- [ ] **Remove `eightpoints/guzzle-bundle` + `idci/graphql-client-bundle`.** - [x] **Remove `eightpoints/guzzle-bundle` + `idci/graphql-client-bundle`.**
They exist only to build one near-static GitHub GraphQL query string They exist only to build one near-static GitHub GraphQL query string
(`GitHubProvider.php:59-74`); the bundle's own HTTP transport is already (`GitHubProvider.php:59-74`); the bundle's own HTTP transport is already
bypassed (comment at `GitHubProvider.php:76-77`). Replace bypassed (comment at `GitHubProvider.php:76-77`).
- [ ] Replace
`$graphqlClient->buildQuery(...)->getGraphQLQuery()` with a plain `$graphqlClient->buildQuery(...)->getGraphQLQuery()` with a plain
`sprintf`/heredoc query string. Delete the two packages from `sprintf`/heredoc query string. Delete the two packages from
`composer.json`, `config/bundles.php`, and delete `composer.json`, `config/bundles.php`, and delete
`config/packages/eight_points_guzzle.yaml` + `config/packages/eight_points_guzzle.yaml` +
`config/packages/idci_graphql_client.yaml`. Run `composer update` and `config/packages/idci_graphql_client.yaml`. Run `composer update` and
commit the regenerated `composer.lock`. commit the regenerated `composer.lock`.
- [ ] **Dedupe `baseUrl` normalization.** `GitLabProvider.php` (lines 41,
- [x] **Dedupe `baseUrl` normalization.** `GitLabProvider.php` (lines 41,
53) and `GiteaProvider.php` (lines 42, 54) each call 53) and `GiteaProvider.php` (lines 42, 54) each call
`rtrim($this->baseUrl..., '/')` twice — once in `ping()`, once in `rtrim($this->baseUrl..., '/')` twice — once in `ping()`, once in
`fetch()`. Compute it once as a `private readonly string $baseUrl` in `fetch()`. Compute it once as a `private readonly string $baseUrl` in
@@ -35,16 +37,16 @@ Order matters — later steps assume earlier ones are done.
## 3. `ContributionStore` (SQLite via native PDO) ## 3. `ContributionStore` (SQLite via native PDO)
- [ ] New `src/Service/ContributionStore.php`. PDO SQLite, DB file at - [x] New `src/Service/ContributionStore.php`. PDO SQLite, DB file at
`%kernel.project_dir%/var/data/contributions.db` (configurable `%kernel.project_dir%/var/data/contributions.db` (configurable
constructor arg), table created lazily: constructor arg), table created lazily:
```sql ```sql
CREATE TABLE IF NOT EXISTS contributions ( CREATE TABLE IF NOT EXISTS contributions (
provider TEXT NOT NULL, provider TEXT NOT NULL,
date TEXT NOT NULL, date TEXT NOT NULL,
count INTEGER NOT NULL, count INTEGER NOT NULL CHECK (count >= 0),
PRIMARY KEY (provider, date) PRIMARY KEY (provider, date)
); ) WITHOUT ROWID, STRICT;
``` ```
- [ ] `latestDate(string $provider): ?string` — `SELECT MAX(date) WHERE provider = ?`. - [ ] `latestDate(string $provider): ?string` — `SELECT MAX(date) WHERE provider = ?`.
- [ ] `merge(string $provider, array $dateCounts): void` — upsert via - [ ] `merge(string $provider, array $dateCounts): void` — upsert via
-2
View File
@@ -5,8 +5,6 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": ">=8.2", "php": ">=8.2",
"eightpoints/guzzle-bundle": "^8.6",
"idci/graphql-client-bundle": "^2.0",
"monolog/monolog": "^3.10", "monolog/monolog": "^3.10",
"symfony/cache": "7.4.*", "symfony/cache": "7.4.*",
"symfony/console": "7.4.*", "symfony/console": "7.4.*",
Generated
+308 -1501
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
eight_points_guzzle:
clients:
github_graphql:
base_url: 'https://api.github.com/graphql'
options:
headers:
Authorization: 'Bearer %env(GITHUB_TOKEN)%'
-4
View File
@@ -1,4 +0,0 @@
idci_graphql_client:
clients:
github:
http_client: 'eight_points_guzzle.client.github_graphql'
+2 -1
View File
@@ -121,7 +121,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* } * }
* @psalm-type ServicesConfig = array{ * @psalm-type ServicesConfig = array{
* _defaults?: DefaultsType, * _defaults?: DefaultsType,
* _instanceof?: InstanceofType, * _instanceof?: array<class-string, InstanceofType>,
* ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null> * ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
* } * }
* @psalm-type ExtensionType = array<string, mixed> * @psalm-type ExtensionType = array<string, mixed>
@@ -857,6 +857,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* http_errors?: bool|Param, * http_errors?: bool|Param,
* expect?: mixed, * expect?: mixed,
* ssl_key?: mixed, * ssl_key?: mixed,
* force_ip_resolve?: mixed, // Default: null
* stream?: bool|Param, * stream?: bool|Param,
* synchronous?: bool|Param, * synchronous?: bool|Param,
* read_timeout?: scalar|Param|null, * read_timeout?: scalar|Param|null,
+2 -1
View File
@@ -6,7 +6,8 @@ services:
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
volumes: volumes:
- .:/app - .:/app
- /app/vendor # keeps vendor from the dev image, not your local dir - ./vendor:/app/vendor
- /app/var/data
environment: environment:
APP_ENV: dev APP_ENV: dev
APP_DEBUG: "1" APP_DEBUG: "1"
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Service;
use PDO;
class ContributionStore {
private PDO $pdo;
public function __construct(
private readonly string $dbPath = "%kernel.project_dir%/var/data/contributions.db",
private readonly ?int $retentionDays = null,
){
if(!is_dir(\dirname($this->dbPath))){
mkdir(\dirname($this->dbPath), 0755, recursive: true);
}
$this->pdo = new PDO('sqlite:' . $this->dbPath);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$this->prepareSchema();
}
private function prepareSchema():void{
$this->pdo->exec('
CREATE TABLE IF NOT EXISTS contributions (
provider TEXT NOT NULL,
date TEXT NOT NULL,
count INTEGER NOT NULL CHECK (count >= 0),
PRIMARY KEY (provider, date)
) WITHOUT ROWID, STRICT
');
}
}
+9 -9
View File
@@ -25,7 +25,9 @@ final class GitLabProvider implements ProviderInterface
private readonly string $token, private readonly string $token,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly string $baseUrl = '', private readonly string $baseUrl = '',
) {} ) {
$this->baseUrl = rtrim($this->baseUrl !== '' ? $this->baseUrl : 'https://gitlab.com', '/');
}
public function getName(): string public function getName(): string
{ {
@@ -39,9 +41,8 @@ final class GitLabProvider implements ProviderInterface
public function ping(): void public function ping(): void
{ {
$baseUrl = rtrim($this->baseUrl !== '' ? $this->baseUrl : 'https://gitlab.com', '/');
$this->client->request('GET', "$baseUrl/api/v4/user", [ $this->client->request('GET', "$this->baseUrl/api/v4/user", [
'headers' => ['PRIVATE-TOKEN' => $this->token], 'headers' => ['PRIVATE-TOKEN' => $this->token],
])->getContent(); ])->getContent();
} }
@@ -56,23 +57,22 @@ final class GitLabProvider implements ProviderInterface
*/ */
public function startFetch(): array public function startFetch(): array
{ {
$baseUrl = rtrim($this->baseUrl !== '' ? $this->baseUrl : 'https://gitlab.com', '/');
$this->logger->debug('GitLabProvider: fetching contributions', ['user' => $this->username, 'url' => $baseUrl]); $this->logger->debug('GitLabProvider: fetching contributions', ['user' => $this->username, 'url' => $this->baseUrl]);
$userResponse = $this->client->request('GET', "$baseUrl/api/v4/users", [ $userResponse = $this->client->request('GET', "$this->baseUrl/api/v4/users", [
'headers' => ['PRIVATE-TOKEN' => $this->token], 'headers' => ['PRIVATE-TOKEN' => $this->token],
'query' => ['username' => $this->username], 'query' => ['username' => $this->username],
]); ]);
$users = $userResponse->toArray(); $users = $userResponse->toArray();
if (empty($users)) { if (empty($users)) {
throw new NotFoundHttpException("GitLab: user '{$this->username}' not found on $baseUrl"); throw new NotFoundHttpException("GitLab: user '{$this->username}' not found on $this->baseUrl");
} }
$userId = $users[0]['id']; $userId = $users[0]['id'];
$after = (new \DateTimeImmutable('-365 days'))->format('Y-m-d'); $after = (new \DateTimeImmutable('-365 days'))->format('Y-m-d');
$response = $this->client->request('GET', "$baseUrl/api/v4/users/$userId/events", [ $response = $this->client->request('GET', "$this->baseUrl/api/v4/users/$userId/events", [
'headers' => ['PRIVATE-TOKEN' => $this->token], 'headers' => ['PRIVATE-TOKEN' => $this->token],
'query' => [ 'query' => [
'after' => $after, 'after' => $after,
@@ -81,7 +81,7 @@ final class GitLabProvider implements ProviderInterface
], ],
]); ]);
return ['baseUrl' => $baseUrl, 'userId' => $userId, 'after' => $after, 'page' => 1, 'response' => $response]; return ['baseUrl' => $this->baseUrl, 'userId' => $userId, 'after' => $after, 'page' => 1, 'response' => $response];
} }
/** /**
+8 -6
View File
@@ -26,7 +26,9 @@ final class GiteaProvider implements ProviderInterface
private readonly string $token, private readonly string $token,
private readonly string $baseUrl, private readonly string $baseUrl,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
) {} ) {
$this->baseUrl = rtrim($this->baseUrl, '/');
}
public function getName(): string public function getName(): string
{ {
@@ -40,20 +42,20 @@ final class GiteaProvider implements ProviderInterface
public function ping(): void public function ping(): void
{ {
$baseUrl = rtrim($this->baseUrl, '/');
$this->client->request('GET', "$baseUrl/api/v1/user", [
$this->client->request('GET', "$this->baseUrl/api/v1/user", [
'headers' => ['Authorization' => "token {$this->token}"], 'headers' => ['Authorization' => "token {$this->token}"],
])->getContent(); ])->getContent();
} }
public function startFetch(): ResponseInterface public function startFetch(): ResponseInterface
{ {
$baseUrl = rtrim($this->baseUrl, '/');
$this->logger->debug('GiteaProvider: fetching contributions', ['user' => $this->username, 'url' => $baseUrl]);
return $this->client->request('GET', "$baseUrl/api/v1/users/{$this->username}/heatmap", [ $this->logger->debug('GiteaProvider: fetching contributions', ['user' => $this->username, 'url' => $this->baseUrl]);
return $this->client->request('GET', "$this->baseUrl/api/v1/users/{$this->username}/heatmap", [
'headers' => ['Authorization' => "token {$this->token}"], 'headers' => ['Authorization' => "token {$this->token}"],
]); ]);
} }