Files
ninemine/src/Service/RedmineService.php
2025-07-11 14:00:54 +02:00

399 lines
14 KiB
PHP

<?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();
}
}
}
// Parent
foreach ($project->getIssues() as $issue) {
$parent = null;
if (array_key_exists('parent', $issue->getRedmine())) {
$parent = $this->issueRepository->find($issue->getRedmine()['parent']['id']);
}
$issue->setParent($parent);
$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' => '*',
];
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] = $this->getIssue($issue['id'], $apiKey);
$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,journals";
$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],
]);
if (200 !== $response->getStatusCode()) {
if (401 === $response->getStatusCode()) {
throw new \RuntimeException('Erreur Redmine ('.$response->getStatusCode().') : Opération non autorisée, avez-vous placé votre apikey redmine sur votre profil');
}
throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode());
}
return $response->toArray();
} 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());
}
}
}