Files
ninemine/src/Service/RedmineService.php
2025-07-08 20:03:19 +02:00

398 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Service;
use App\Entity\Issue;
use App\Entity\Project;
use App\Repository\IssueRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class RedmineService
{
private string $baseUrl;
private HttpClientInterface $client;
private IssueRepository $issueRepository;
private EntityManagerInterface $em;
public function __construct(HttpClientInterface $client, ParameterBagInterface $params, IssueRepository $issueRepository, EntityManagerInterface $em)
{
$this->client = $client;
$this->baseUrl = rtrim($params->get('redmineUrl'), '/');
$this->issueRepository = $issueRepository;
$this->em = $em;
}
/**
* Récupère les projets accessibles via l'API key.
*/
public function getProjects(string $apiKey): array
{
try {
$response = $this->client->request('GET', $this->baseUrl.'/projects.json', [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
return $data['projects'] ?? [];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function getProject(int|string $id, string $apiKey): ?array
{
try {
$response = $this->client->request('GET', $this->baseUrl.'/projects/'.$id.'.json?include=trackers,issue_categories,issue_custom_fields', [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
$status = $response->getStatusCode();
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
$data['project']['issue_statuses'] = $this->getIssueStatuses($apiKey);
$data['project']['issue_priorities'] = $this->getIssuePriorities($apiKey);
$data['project']['versions'] = $this->getProjectVersions($id, $apiKey);
if (empty($data['project']['versions']) && array_key_exists('parent', $data['project'])) {
$data['project']['versions'] = $this->getProjectSprints($data['project']['parent']['id'], $apiKey);
}
$data['project']['sprints'] = $this->getProjectSprints($id, $apiKey);
if (empty($data['project']['sprints']) && array_key_exists('parent', $data['project'])) {
$data['project']['sprints'] = $this->getProjectSprints($data['project']['parent']['id'], $apiKey);
}
return $data['project'] ?? null;
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function getIssueStatuses(string $apiKey): array
{
try {
$response = $this->client->request('GET', $this->baseUrl.'/issue_statuses.json', [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
return $response->toArray()['issue_statuses'] ?? [];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function getIssuePriorities(string $apiKey): array
{
try {
$response = $this->client->request('GET', $this->baseUrl.'/enumerations/issue_priorities.json', [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
return $data['issue_priorities'] ?? [];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function getProjectVersions(int $id, string $apiKey): array
{
try {
$response = $this->client->request('GET', $this->baseUrl.'/projects/'.$id.'/versions.json', [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
return $data['versions'];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function getProjectSprints(int $projectId, string $apiKey): array
{
try {
$url = "{$this->baseUrl}/projects/{$projectId}/agile_sprints.json";
$response = $this->client->request('GET', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
return $data['sprints'] ?? [];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function majProjectIssues(Project $project, string $apiKey, bool $force = false)
{
$rissues = $this->getProjectIssues($project->getId(), $apiKey, $force ? null : $project->getUpdateAt());
$rissueids = [];
foreach ($rissues as $rissue) {
array_push($rissueids, $rissue['id']);
$issue = $this->issueRepository->find($rissue['id']);
if (!$issue) {
$issue = new Issue();
$issue->setId($rissue['id']);
}
$issue->setRedmine($rissue);
$issue->setProject($project);
// Calcul de la position du status
$issueStatusId = $rissue['status']['id'];
$statusPosition = 0;
foreach ($project->getRedmine()['issue_statuses'] as $index => $status) {
if ($status['id'] === $issueStatusId) {
$statusPosition = $index; // ou $index + 1 si tu veux position humaine
break;
}
}
$issue->setRowstatus($statusPosition);
// Calcul de la position version
if (isset($rissue['fixed_version'])) {
$issue->setRowversion($rissue['fixed_version']['name']);
} else {
$issue->setRowversion('');
}
// Calcul de la position sprint
$issueSprintId = $rissue['sprint']['agile_sprint_id'];
$sprintPosition = '';
$sprintIndex = null;
foreach ($project->getRedmine()['sprints'] as $key => $sprint) {
if ($sprint['id'] === $issueSprintId) {
$sprintPosition = $sprint['id'];
$sprintIndex = $key;
break;
}
}
$issue->setRowsprint($sprintPosition);
// Calcul position issue
if (isset($rissue['sprint'])) {
$issue->setRowissue($rissue['sprint']['position'] ?? 1000000);
} else {
$issue->setRowissue(1000000);
}
$this->em->persist($issue);
$project->setUpdateAt(new \DateTime());
$this->em->flush();
}
if ($force) {
foreach ($project->getIssues() as $issue) {
if (!in_array($issue->getId(), $rissueids)) {
$this->em->remove($issue);
$this->em->flush();
}
}
}
}
public function getProjectIssues(int $projectId, string $apiKey, ?\DateTimeInterface $updatedSince = null): array
{
$limit = 100;
$offset = 0;
$allIssues = [];
do {
$queryParams = [
'project_id' => $projectId,
'limit' => $limit,
'offset' => $offset,
'status_id' => '*', // Inclure toutes les issues
];
if (null !== $updatedSince) {
$queryParams['updated_on'] = '>='.$updatedSince->format('Y-m-d\TH:i:s\Z');
}
$url = $this->baseUrl.'/issues.json?'.http_build_query($queryParams);
$response = $this->client->request('GET', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur API Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
$issues = $data['issues'] ?? [];
foreach ($issues as $key => $issue) {
$issues[$key]['sprint'] = $this->getIssueAgile($issue['id'], $apiKey);
}
$allIssues = array_merge($allIssues, $issues);
$offset += $limit;
$totalCount = $data['total_count'] ?? 0;
} while ($offset < $totalCount);
return $allIssues;
}
public function getIssue(int $issueId, string $apiKey): array
{
try {
$url = "{$this->baseUrl}/issues/{$issueId}.json?include=allowed_statuses";
$response = $this->client->request('GET', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
$data['issue']['sprint'] = $this->getIssueAgile($issueId, $apiKey);
return $data['issue'] ?? [];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function getIssueAgile(int $issueId, string $apiKey): array
{
try {
$url = "{$this->baseUrl}/issues/{$issueId}/agile_data.json";
$response = $this->client->request('GET', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Accept' => 'application/json',
],
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
$data = $response->toArray();
return $data['agile_data'] ?? [];
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function updateIssue(int $id, array $data, string $apiKey): array
{
$url = $this->baseUrl.'/issues/'.$id.'.json';
try {
$response = $this->client->request('PUT', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => ['issue' => $data],
]);
$statusCode = $response->getStatusCode();
$content = trim($response->getContent(false));
// Si vide et code 200, cest peut-être une réussite silencieuse
if ('' === $content) {
return ['success' => true, 'message' => 'OK, mais pas de contenu'];
}
$decoded = json_decode($content, true);
if (isset($decoded['errors']) && is_array($decoded['errors']) && count($decoded['errors']) > 0) {
throw new \RuntimeException('Erreur Redmine : '.implode(', ', $decoded['errors']));
}
return $decoded;
} catch (ClientExceptionInterface|ServerExceptionInterface $e) {
// Erreur HTTP (4xx ou 5xx)
$errorBody = $e->getResponse()->getContent(false);
throw new \RuntimeException('Erreur Redmine: '.$errorBody, $e->getCode(), $e);
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
public function updatePosition(array $data, string $apiKey): array
{
$url = $this->baseUrl.'/agile/board';
try {
$response = $this->client->request('PUT', $url, [
'headers' => [
'X-Redmine-API-Key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => ['issue' => $data],
]);
return ['ok'];
} catch (ClientExceptionInterface|ServerExceptionInterface $e) {
// Erreur HTTP (4xx ou 5xx)
$errorBody = $e->getResponse()->getContent(false);
throw new \RuntimeException('Erreur Redmine: '.$errorBody, $e->getCode(), $e);
} catch (TransportExceptionInterface $e) {
throw new \RuntimeException('Erreur de communication avec Redmine : '.$e->getMessage());
}
}
}