399 lines
14 KiB
PHP
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());
|
|
}
|
|
}
|
|
}
|