cache = $cache; $this->router = $router; $this->httpClient = $client; $this->config = $config; $this->teamService = $teamService; } /** * @param int $teamId * @param bool $forceReload * @return KanbanBoard */ public function getKanbanBoard(int $teamId, bool $forceReload = false): KanbanBoard { $team = $this->teamService->getTeam($teamId); $teamName = $team->getName(); $kanbanBoard = $this->cache->getItem(sprintf("%s-%s", self::CACHE_KEY_KANBANBOARD, $teamName)); if ($forceReload || null === $kanbanBoard) { $user = $this->config->get('jira.user'); $password = $this->config->get('jira.password'); /** @var Config $kanbanBoardUriParams */ $kanbanBoardUri = $this->config->get('url.jiraKanbanBoard'); $kanbanBoardFilter = $this->config->get('jira.filterFields')->toArray(); $kanbanBoardUri = sprintf( $kanbanBoardUri, $team->getFilterId(), implode(",", $kanbanBoardFilter) ); $response = $this->httpClient ->setUri($kanbanBoardUri) ->setAuth($user, $password) ->send(); if (!$response->isSuccess()) { throw new \UnexpectedValueException(sprintf( "Bad JIRA result for URL:\n%s", $kanbanBoardUri ), $response->getStatusCode()); } $parsedJsonData = Decoder::decode($response->getBody(), Json::TYPE_ARRAY); $kanbanBoard = $this->hydrateKanbanBoard($team, $parsedJsonData); $this->cache->setItem(sprintf("%s-%s", self::CACHE_KEY_KANBANBOARD, $teamName), serialize($kanbanBoard)); return $kanbanBoard; } return unserialize($kanbanBoard); } /** * @param int $teamId * @param bool $forceReload * @return array * @throws \Exception */ public function getTeamWatchedIssues(int $teamId, bool $forceReload = false) { $team = $this->teamService->getTeam($teamId); $teamName = $team->getName(); $watchedIssues = $this->cache->getItem(sprintf("%s-%s", self::CACHE_KEY_WATCHED, $teamName)); if ($forceReload || null === $watchedIssues) { $members = array_map(function (array $member): string { return $member['signum']; }, $team->getMembers()); $preparedMembers = sprintf('"%s"', implode('","', $members)); $filter = sprintf( self::WATCH_FILTER, sprintf('"%s"', implode('","', self::IGNORED_STATUSES)), $preparedMembers, $preparedMembers ); $user = $this->config->get('jira.user'); $password = $this->config->get('jira.password'); /** @var Config $kanbanBoardUriParams */ $jiraWatchedIssues = $this->config->get('url.jiraWatchedIssues'); $kanbanBoardFilterFields = [ 'assignee', 'summary', 'comment', ]; $issueFields = implode(",", $kanbanBoardFilterFields); $jiraIssueUri = sprintf($jiraWatchedIssues, $filter, $issueFields); $response = $this->httpClient ->setUri($jiraIssueUri) ->setAuth($user, $password) ->send(); if (!$response->isSuccess()) { throw new \UnexpectedValueException(sprintf( "Bad JIRA result for URL:\n%s", $jiraIssueUri ), $response->getStatusCode()); } $watchedIssues = $this->hydrateWatchedIssues(Decoder::decode($response->getBody(), Json::TYPE_ARRAY), $members); $this->cache->setItem(sprintf("%s-%s", self::CACHE_KEY_WATCHED, $teamName), serialize($watchedIssues)); return $watchedIssues; } return unserialize($watchedIssues); } /** * @param array $parsedJson * @param array $members * @return array * @throws \Exception */ private function hydrateWatchedIssues(array $parsedJson, array $members) { /** @var WatchedIssue[] $hydratedResult */ $hydratedResult = []; foreach ($parsedJson['issues'] as $issueJson) { $issueItem = new WatchedIssue(); $issueItem->setIssue($issueJson['key']) ->setAssignee($issueJson['fields']['assignee']['name']) ->setSummary(html_entity_decode($issueJson['fields']['summary'])); $issueComments = []; foreach ($issueJson['fields']['comment']['comments'] as $commentJson) { $issueComment = new WatchedIssueComment(); $issueComment->setSignum($commentJson['updateAuthor']['name']) ->setName($commentJson['updateAuthor']['displayName']) ->setContent(html_entity_decode($commentJson['body'])) ->setDate(new \DateTimeImmutable($commentJson['updated'])); $issueComments[] = $issueComment; } usort($issueComments, function(WatchedIssueComment $a, WatchedIssueComment $b) { return $a->getDate() <=> $b->getDate(); }); $lastComment = array_pop($issueComments); unset($issueComments); $issueItem->setComment($lastComment); $hydratedResult[] = $issueItem; } /** * sanity check, we only want items where last change was a comment, but that is not possible * with JIRA jql at the moment. */ return array_filter($hydratedResult, function(WatchedIssue $issue) use ($members) { return null !== $issue->getComment() ? !in_array($issue->getComment()->getSignum(), $members) : false; }); } /** * @param string $parentKey * @return null|string */ private function getEpicNameFromParent(string $parentKey): ?string { if (array_key_exists($parentKey, $this->cachedEpics)) { return $this->cachedEpics[$parentKey]; } $user = $this->config->get('jira.user'); $password = $this->config->get('jira.password'); /** @var Config $kanbanBoardUriParams */ $jiraIssueBaseUrl = $this->config->get('url.jiraIssue'); $kanbanBoardFilter = $this->config->get('jira.filterFields')->toArray(); $kanbanBoardFilterString = implode(",", $kanbanBoardFilter); $jiraIssueUri = sprintf($jiraIssueBaseUrl, $parentKey, $kanbanBoardFilterString); $response = $this->httpClient ->setUri($jiraIssueUri) ->setAuth($user, $password) ->send(); if (!$response->isSuccess()) { throw new \UnexpectedValueException("Bad JIRA result: $jiraIssueUri", $response->getStatusCode()); } $parsedJsonParentData = Decoder::decode($response->getBody(), Json::TYPE_ARRAY); if ($parsedJsonParentData['fields'][self::EPIC_TICKET_LINK]) { $jiraIssueUri = sprintf( $jiraIssueBaseUrl, $parsedJsonParentData['fields'][self::EPIC_TICKET_LINK], $kanbanBoardFilterString ); $response = $this->httpClient ->setUri($jiraIssueUri) ->setAuth($user, $password) ->send(); if (!$response->isSuccess()) { throw new \UnexpectedValueException("Bad JIRA result", $response->getStatusCode()); } $parsedJsonEpicData = Decoder::decode($response->getBody(), Json::TYPE_ARRAY); $this->cachedEpics[$parentKey] = $parsedJsonEpicData['fields'][self::EPIC_NAME_FIELD]; return $this->cachedEpics[$parentKey]; } $this->cachedEpics[$parentKey] = null; return null; } /** * @param Team $team * @param $parsedJsonData * @return KanbanBoard * @todo check if avatar has to be locally cached */ private function hydrateKanbanBoard(Team $team, $parsedJsonData): KanbanBoard { $kanbanBoard = new KanbanBoard(); $teamBacklogColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getBacklogColumn()["jiraStatusName"]); $teamInprogressColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getInprogressColumn()["jiraStatusName"]); $teamVerificationColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getVerificationColumn()["jiraStatusName"]); $teamDoneColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getDoneColumn()["jiraStatusName"]); foreach ($parsedJsonData['issues'] as $jsonIssue) { set_time_limit(30); $kanbanEntry = new KanbanEntry(); $kanbanEntry->setId(intval($jsonIssue['id'])) ->setKey($jsonIssue['key']) ->setSummary($jsonIssue['fields']['summary']) ->setIssuePriority($jsonIssue['fields']['priority']['name']) ->setIssuePriorityIcon($jsonIssue['fields']['priority']['iconUrl']) ->setLabels($jsonIssue['fields']['labels']) ->setFixVersions($jsonIssue['fields']['fixVersions']); $spikeTimeSpent = 0; array_map(function ($worklog) use (&$spikeTimeSpent) { $spikeTimeSpent += strtoupper($worklog['comment']) == 'BLOCKED' ? 0 : $worklog['timeSpentSeconds']; }, $jsonIssue['fields']['worklog']['worklogs']); $kanbanEntry->setWorklog((int)ceil($spikeTimeSpent / 3600)); $secondsBlocked = 0; array_map(function ($worklog) use (&$secondsBlocked) { $secondsBlocked += strtoupper($worklog['comment']) == 'BLOCKED' ? $worklog['timeSpentSeconds'] : 0; }, $jsonIssue['fields']['worklog']['worklogs']); $kanbanEntry->setDaysBlocked((int)ceil($secondsBlocked / 28800)); // additional assignees : customfield_10401 if (isset($jsonIssue['fields']['customfield_10401'])) { foreach ($jsonIssue['fields']['customfield_10401'] as $assignee) { $avatarUrl = $this->router->generateUri('avatar.image', [ 'signum' => $assignee['key'], ]); $jiraAssignee = new JiraAssignee(); $jiraAssignee->setName($assignee['displayName']) ->setSignum($assignee['key']) ->setEmail(strtolower($assignee['emailAddress'])) ->setAvatar($avatarUrl) ->setActive($assignee['active']); $kanbanEntry->addAdditionalAssignee($jiraAssignee); } } // epicName: have to fetch 2 extra records if (isset($jsonIssue['fields'][self::EPIC_TICKET_LINK])) { $epicName = $this->getEpicNameFromParent($jsonIssue['key']); $kanbanEntry->setEpicName($epicName); } elseif (isset($jsonIssue['fields']['parent'])) { $epicName = $this->getEpicNameFromParent($jsonIssue['fields']['parent']['key']); $kanbanEntry->setEpicName($epicName); } // jira status $jiraStatus = new JiraStatus(); $jiraStatus->setName($jsonIssue['fields']['status']['name']) ->setColor($jsonIssue['fields']['status']['statusCategory']['colorName']); $kanbanEntry->setStatus($jiraStatus); // assignee if ($jsonIssue['fields']['assignee']) { $avatarUrl = $this->router->generateUri('avatar.image', [ 'signum' => $jsonIssue['fields']['assignee']['key'], ]); $jiraAssignee = new JiraAssignee(); $jiraAssignee->setName($jsonIssue['fields']['assignee']['displayName']) ->setSignum($jsonIssue['fields']['assignee']['key']) ->setEmail(strtolower($jsonIssue['fields']['assignee']['emailAddress'])) ->setAvatar($avatarUrl) ->setActive($jsonIssue['fields']['assignee']['active']); $kanbanEntry->setAssignee($jiraAssignee); unset($jiraAssignee); } // issue type if ($jsonIssue['fields']['issuetype']) { $jiraIssueType = new JiraIssueType(); $jiraIssueType->setName($jsonIssue['fields']['issuetype']['name']) ->setDescription($jsonIssue['fields']['issuetype']['description']) ->setIcon($jsonIssue['fields']['issuetype']['iconUrl']); $kanbanEntry->setIssueType($jiraIssueType); unset($jiraIssueType); } $kanbanEntry->setUpdatedAt(new \DateTime($jsonIssue['fields']['updated'])); if (in_array($jiraStatus->getName(), $teamBacklogColumns)) { $kanbanBoard->addInbox($kanbanEntry); } elseif (in_array($jiraStatus->getName(), $teamInprogressColumns)) { $kanbanBoard->addInProgress($kanbanEntry); } elseif (in_array($jiraStatus->getName(), $teamVerificationColumns)) { $kanbanBoard->addVerification($kanbanEntry); } elseif (in_array($jiraStatus->getName(), $teamDoneColumns)) { $kanbanBoard->addDone($kanbanEntry); } unset($kanbanEntry); } return $kanbanBoard; } }