client = $client; $this->baseUrl = rtrim($params->get('redmineUrl'), '/'); $this->issueRepository = $issueRepository; $this->em = $em; } 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 majProject(Project $project, string $apiKey, bool $force = false): void { $updatedAt = $project->getUpdateAt(); $updatedMinus10 = $updatedAt instanceof \DateTime ? (clone $updatedAt)->sub(new \DateInterval('PT10M')) : null; $redmine = $this->getProject($project->getId(), $apiKey, $updatedMinus10, $force); if ($redmine) { $project->setRedmine($redmine); } $project->setUpdateIssuesAt(new \DateTime()); $this->em->flush(); } public function getProject(int|string $id, string $apiKey, \DateTimeInterface $date, bool $force = false): ?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', ], ]); if (200 !== $response->getStatusCode()) { throw new \RuntimeException('Erreur de communication avec Redmine : '.$response->getStatusCode()); } $data = $response->toArray(); if (!$force && $date && $data['project']['updated_on'] <= $date) { return null; } $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) { $this->majProject($project, $apiKey, $force); $updatedAt = $project->getUpdateIssuesAt(); $updatedMinus10 = $updatedAt instanceof \DateTime ? (clone $updatedAt)->sub(new \DateInterval('PT10M')) : null; $rissues = $this->getProjectIssues($project->getId(), $apiKey, $force ? null : $updatedMinus10); $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->setRowissue(1000000); } $issue->setRedmine($rissue); $issue->setProject($project); $issue->setIsClosed($rissue['status']['is_closed']); // 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 = plus géré via redmine /* if (isset($rissue['sprint'])) { $issue->setRowissue($rissue['sprint']['position'] ?? 1000000); } else { $issue->setRowissue(1000000); } */ $this->em->persist($issue); $project->setUpdateIssuesAt(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() && 204 !== $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 []; } 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()); } } }