Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/GithubApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,110 @@ public function listIssueComments(
return new GithubCommentList($comments);
}

/**
* List events on an issue (labeled, assigned, closed, etc.).
*
* Backed by `GET /repos/{owner}/{repo}/issues/{issue_number}/events`.
* Paginates via Link headers.
*/
public function getIssueEvents(GithubRepository $repo, int $issueNumber): GithubIssueEventList
{
$requestFactory = new ListIssueEventsRequestFactory($this->requestFactory, $this->config);
$request = $requestFactory->create($repo, $issueNumber);
$events = [];
$factory = new GithubIssueEventFactory();
while (true) {
$response = $this->httpClient->sendRequest($request);
if ($response->getStatusCode() !== 200) {
throw new Exception($this->parseErrorResponse($response));
}
$data = json_decode((string) $response->getBody());
if (is_array($data)) {
foreach ($data as $eventData) {
if (is_object($eventData)) {
$events[] = $factory->createFromApiResponse($eventData);
}
}
}
$pagination = new GithubApiPagination($request, $response);
if (!$pagination->hasNextLink()) {
break;
}
$request = $pagination->nextRequest();
}
return new GithubIssueEventList($events);
}

/**
* List the timeline of an issue.
*
* Backed by `GET /repos/{owner}/{repo}/issues/{issue_number}/timeline`.
* Broader than `getIssueEvents()`: includes cross-references,
* comments, and PR review activity in addition to issue events.
* Paginates via Link headers.
*/
public function getIssueTimeline(GithubRepository $repo, int $issueNumber): GithubTimelineEventList
{
$requestFactory = new ListIssueTimelineRequestFactory($this->requestFactory, $this->config);
$request = $requestFactory->create($repo, $issueNumber);
$events = [];
$factory = new GithubTimelineEventFactory();
while (true) {
$response = $this->httpClient->sendRequest($request);
if ($response->getStatusCode() !== 200) {
throw new Exception($this->parseErrorResponse($response));
}
$data = json_decode((string) $response->getBody());
if (is_array($data)) {
foreach ($data as $eventData) {
if (is_object($eventData)) {
$events[] = $factory->createFromApiResponse($eventData);
}
}
}
$pagination = new GithubApiPagination($request, $response);
if (!$pagination->hasNextLink()) {
break;
}
$request = $pagination->nextRequest();
}
return new GithubTimelineEventList($events);
}

/**
* List repository activity events.
*
* Backed by `GET /repos/{owner}/{repo}/events`. Paginates via
* Link headers.
*/
public function getRepositoryEvents(GithubRepository $repo): GithubRepositoryEventList
{
$requestFactory = new ListRepositoryEventsRequestFactory($this->requestFactory, $this->config);
$request = $requestFactory->create($repo);
$events = [];
$factory = new GithubRepositoryEventFactory();
while (true) {
$response = $this->httpClient->sendRequest($request);
if ($response->getStatusCode() !== 200) {
throw new Exception($this->parseErrorResponse($response));
}
$data = json_decode((string) $response->getBody());
if (is_array($data)) {
foreach ($data as $eventData) {
if (is_object($eventData)) {
$events[] = $factory->createFromApiResponse($eventData);
}
}
}
$pagination = new GithubApiPagination($request, $response);
if (!$pagination->hasNextLink()) {
break;
}
$request = $pagination->nextRequest();
}
return new GithubRepositoryEventList($events);
}

public function listPullRequests(GithubRepository $repo, string $baseBranch = '', string $headRef = '', string $state = 'open'): GithubPullRequestList
{
$pullRequests = [];
Expand Down
62 changes: 62 additions & 0 deletions src/GithubIssueEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Horde\GithubApiClient;

use Stringable;

/**
* Represents a single event in an issue's event timeline (labeled,
* assigned, closed, etc.), as returned by
* `GET /repos/{owner}/{repo}/issues/{issue_number}/events`.
*
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @package GithubApiClient
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
*/
class GithubIssueEvent implements Stringable
{
public function __construct(
public readonly int $id,
public readonly string $event,
public readonly ?GithubUser $actor,
public readonly string $createdAt,
public readonly string $apiUrl,
public readonly string $commitId = '',
public readonly string $commitUrl = '',
public readonly string $nodeId = '',
) {}

public function __toString(): string
{
return $this->event;
}

/**
* Create GithubIssueEvent from GitHub API response
*
* @param object $data Decoded JSON event object from API
* @return self
*/
public static function fromApiResponse(object $data): self
{
$userFactory = new GithubUserFactory();

return new self(
id: $data->id ?? 0,
event: $data->event ?? '',
actor: isset($data->actor) ? $userFactory->createFromApiResponse($data->actor) : null,
createdAt: $data->created_at ?? '',
apiUrl: $data->url ?? '',
commitId: $data->commit_id ?? '',
commitUrl: $data->commit_url ?? '',
nodeId: $data->node_id ?? '',
);
}
}
31 changes: 31 additions & 0 deletions src/GithubIssueEventFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Horde\GithubApiClient;

/**
* Factory for creating GithubIssueEvent objects from API responses
*
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @package GithubApiClient
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
*/
class GithubIssueEventFactory
{
/**
* Create GithubIssueEvent from GitHub API response
*
* @param object $data Decoded JSON event object from API
* @return GithubIssueEvent
*/
public function createFromApiResponse(object $data): GithubIssueEvent
{
return GithubIssueEvent::fromApiResponse($data);
}
}
72 changes: 72 additions & 0 deletions src/GithubIssueEventList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Horde\GithubApiClient;

use Countable;
use Iterator;

/**
* Collection of GitHub issue events
*
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @package GithubApiClient
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
*/
class GithubIssueEventList implements Iterator, Countable
{
private int $position = 0;

/**
* @param array<GithubIssueEvent> $events
*/
public function __construct(
private array $events = []
) {}

public function current(): GithubIssueEvent
{
return $this->events[$this->position];
}

public function key(): int
{
return $this->position;
}

public function next(): void
{
++$this->position;
}

public function rewind(): void
{
$this->position = 0;
}

public function valid(): bool
{
return isset($this->events[$this->position]);
}

public function count(): int
{
return count($this->events);
}

/**
* Get all events as array
*
* @return array<GithubIssueEvent>
*/
public function toArray(): array
{
return $this->events;
}
}
68 changes: 68 additions & 0 deletions src/GithubRepositoryEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Horde\GithubApiClient;

use Stringable;

/**
* Represents a single repository activity event, as returned by
* `GET /repos/{owner}/{repo}/events`.
*
* Unlike issue events, repository events use a lighter-weight actor
* shape (no `html_url`) and carry a polymorphic `payload` that varies
* by event `type` (e.g. `PushEvent`, `IssuesEvent`, `WatchEvent`).
* The payload is exposed as a decoded array rather than a typed
* object.
*
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @package GithubApiClient
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
*/
class GithubRepositoryEvent implements Stringable
{
/**
* @param array<string, mixed> $payload Decoded event payload
*/
public function __construct(
public readonly string $id,
public readonly string $type,
public readonly ?GithubUser $actor,
public readonly string $repoName,
public readonly array $payload,
public readonly bool $public,
public readonly string $createdAt,
) {}

public function __toString(): string
{
return $this->type;
}

/**
* Create GithubRepositoryEvent from GitHub API response
*
* @param object $data Decoded JSON event object from API
* @return self
*/
public static function fromApiResponse(object $data): self
{
$userFactory = new GithubUserFactory();

return new self(
id: $data->id ?? '',
type: $data->type ?? '',
actor: isset($data->actor) ? $userFactory->createFromApiResponse($data->actor) : null,
repoName: $data->repo->name ?? '',
payload: isset($data->payload) ? (array) $data->payload : [],
public: $data->public ?? true,
createdAt: $data->created_at ?? '',
);
}
}
31 changes: 31 additions & 0 deletions src/GithubRepositoryEventFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Horde\GithubApiClient;

/**
* Factory for creating GithubRepositoryEvent objects from API responses
*
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @package GithubApiClient
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
*/
class GithubRepositoryEventFactory
{
/**
* Create GithubRepositoryEvent from GitHub API response
*
* @param object $data Decoded JSON event object from API
* @return GithubRepositoryEvent
*/
public function createFromApiResponse(object $data): GithubRepositoryEvent
{
return GithubRepositoryEvent::fromApiResponse($data);
}
}
Loading
Loading