-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Summary
Implement a comprehensive comment management system with support for creating, updating, deleting comments, managing reactions, and querying issue timelines. Should follow the fluent API patterns established in conduit-ui/repo.
Requirements
Comment DTO
File: src/Data/Comment.php
namespace ConduitUI\Issue\Data;
use Carbon\Carbon;
use Illuminate\Support\Collection;
readonly class Comment
{
public function __construct(
public int $id,
public int $issueNumber,
public string $body,
public User $author,
public Carbon $createdAt,
public Carbon $updatedAt,
public string $htmlUrl,
public string $apiUrl,
public Collection $reactions, // Collection<Reaction>
) {}
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
issueNumber: $data['issue_url']
? (int) basename($data['issue_url'])
: 0,
body: $data['body'] ?? '',
author: User::fromArray($data['user']),
createdAt: Carbon::parse($data['created_at']),
updatedAt: Carbon::parse($data['updated_at']),
htmlUrl: $data['html_url'],
apiUrl: $data['url'],
reactions: isset($data['reactions'])
? Reaction::collectionFromArray($data['reactions'])
: collect(),
);
}
}Reaction DTO
File: src/Data/Reaction.php
namespace ConduitUI\Issue\Data;
use Carbon\Carbon;
use Illuminate\Support\Collection;
readonly class Reaction
{
public const PLUS_ONE = '+1';
public const MINUS_ONE = '-1';
public const LAUGH = 'laugh';
public const CONFUSED = 'confused';
public const HEART = 'heart';
public const HOORAY = 'hooray';
public const ROCKET = 'rocket';
public const EYES = 'eyes';
public function __construct(
public int $id,
public string $content, // +1, -1, laugh, confused, heart, hooray, rocket, eyes
public User $user,
public Carbon $createdAt,
) {}
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
content: $data['content'],
user: User::fromArray($data['user']),
createdAt: Carbon::parse($data['created_at']),
);
}
public static function collectionFromArray(array $data): Collection
{
// GitHub returns reactions as counts, not individual objects
// We'll need to handle both formats
if (isset($data['url'])) {
// Individual reaction format
return collect([self::fromArray($data)]);
}
// Summary format - convert to collection
return collect();
}
public function isPlusOne(): bool
{
return $this->content === self::PLUS_ONE;
}
public function isMinusOne(): bool
{
return $this->content === self::MINUS_ONE;
}
}CommentManager Service
File: src/Services/CommentManager.php
namespace ConduitUI\Issue\Services;
use ConduitUI\Connector\GitHub;
use ConduitUI\Issue\Data\Comment;
use ConduitUI\Issue\Data\Reaction;
use Illuminate\Support\Collection;
final class CommentManager
{
public function __construct(
protected GitHub $github,
protected string $fullName,
protected int $issueNumber,
) {}
/**
* Get all comments
*/
public function all(): Collection
{
$response = $this->github->get(
"/repos/{$this->fullName}/issues/{$this->issueNumber}/comments"
);
return collect($response->json())
->map(fn($comment) => Comment::fromArray($comment));
}
/**
* Get a specific comment
*/
public function find(int $commentId): Comment
{
$response = $this->github->get(
"/repos/{$this->fullName}/issues/comments/{$commentId}"
);
return Comment::fromArray($response->json());
}
/**
* Create a new comment
*/
public function create(string $body): CommentInstance
{
$response = $this->github->post(
"/repos/{$this->fullName}/issues/{$this->issueNumber}/comments",
['body' => $body]
);
$comment = Comment::fromArray($response->json());
return new CommentInstance(
github: $this->github,
fullName: $this->fullName,
commentId: $comment->id,
);
}
/**
* Update a comment
*/
public function update(int $commentId, string $body): Comment
{
$response = $this->github->patch(
"/repos/{$this->fullName}/issues/comments/{$commentId}",
['body' => $body]
);
return Comment::fromArray($response->json());
}
/**
* Delete a comment
*/
public function delete(int $commentId): bool
{
$response = $this->github->delete(
"/repos/{$this->fullName}/issues/comments/{$commentId}"
);
return $response->successful();
}
/**
* Get comment instance for chaining
*/
public function comment(int $commentId): CommentInstance
{
return new CommentInstance(
github: $this->github,
fullName: $this->fullName,
commentId: $commentId,
);
}
}CommentInstance Service
File: src/Services/CommentInstance.php
namespace ConduitUI\Issue\Services;
use ConduitUI\Connector\GitHub;
use ConduitUI\Issue\Data\Comment;
use ConduitUI\Issue\Data\Reaction;
use Illuminate\Support\Collection;
final class CommentInstance
{
protected ?Comment $comment = null;
public function __construct(
protected GitHub $github,
protected string $fullName,
protected int $commentId,
) {}
/**
* Get the comment data
*/
public function get(): Comment
{
if ($this->comment === null) {
$this->comment = $this->fetch();
}
return $this->comment;
}
/**
* Update comment body
*/
public function update(string $body): self
{
$response = $this->github->patch(
"/repos/{$this->fullName}/issues/comments/{$this->commentId}",
['body' => $body]
);
$this->comment = Comment::fromArray($response->json());
return $this;
}
/**
* Delete the comment
*/
public function delete(): bool
{
$response = $this->github->delete(
"/repos/{$this->fullName}/issues/comments/{$this->commentId}"
);
return $response->successful();
}
/**
* Add a reaction to the comment
*/
public function react(string $content): Reaction
{
$response = $this->github->post(
"/repos/{$this->fullName}/issues/comments/{$this->commentId}/reactions",
['content' => $content],
['Accept' => 'application/vnd.github+json']
);
return Reaction::fromArray($response->json());
}
/**
* Get all reactions on the comment
*/
public function reactions(): Collection
{
$response = $this->github->get(
"/repos/{$this->fullName}/issues/comments/{$this->commentId}/reactions",
[],
['Accept' => 'application/vnd.github+json']
);
return collect($response->json())
->map(fn($reaction) => Reaction::fromArray($reaction));
}
/**
* Delete a reaction
*/
public function deleteReaction(int $reactionId): bool
{
$response = $this->github->delete(
"/repos/{$this->fullName}/issues/comments/{$this->commentId}/reactions/{$reactionId}",
[],
['Accept' => 'application/vnd.github+json']
);
return $response->successful();
}
/**
* Fetch comment from API
*/
protected function fetch(): Comment
{
$response = $this->github->get(
"/repos/{$this->fullName}/issues/comments/{$this->commentId}"
);
return Comment::fromArray($response->json());
}
}Timeline Events
File: src/Services/TimelineQuery.php
namespace ConduitUI\Issue\Services;
use ConduitUI\Connector\GitHub;
use Illuminate\Support\Collection;
final class TimelineQuery
{
public function __construct(
protected GitHub $github,
protected string $fullName,
protected int $issueNumber,
) {}
/**
* Get all timeline events for an issue
*/
public function get(): Collection
{
$response = $this->github->get(
"/repos/{$this->fullName}/issues/{$this->issueNumber}/timeline",
[],
['Accept' => 'application/vnd.github.mockingbird-preview+json']
);
return collect($response->json());
}
/**
* Get only specific event types
*/
public function ofType(string|array $types): Collection
{
$types = is_array($types) ? $types : [$types];
return $this->get()
->filter(fn($event) => in_array($event['event'] ?? null, $types));
}
/**
* Get only comment events
*/
public function comments(): Collection
{
return $this->ofType('commented');
}
/**
* Get only label events
*/
public function labels(): Collection
{
return $this->ofType(['labeled', 'unlabeled']);
}
}Usage Examples
use ConduitUI\Issue\Facades\Issues;
use ConduitUI\Issue\Data\Reaction;
// Create and manage comments
$comment = Issues::find('owner/repo', 123)
->comments()
->create('This is a great idea!');
// Update comment
Issues::find('owner/repo', 123)
->comments()
->update($commentId, 'Updated comment text');
// Delete comment
Issues::find('owner/repo', 123)
->comments()
->delete($commentId);
// Chained comment actions
Issues::find('owner/repo', 123)
->comments()
->create('Great work!')
->react(Reaction::PLUS_ONE);
// Get all comments
$comments = Issues::find('owner/repo', 123)
->comments()
->all();
// Add reactions
Issues::find('owner/repo', 123)
->comments()
->comment($commentId)
->react(Reaction::HEART);
// Get reactions
$reactions = Issues::find('owner/repo', 123)
->comments()
->comment($commentId)
->reactions();
// Timeline events
$timeline = Issues::find('owner/repo', 123)
->timeline()
->get();
$comments = Issues::find('owner/repo', 123)
->timeline()
->comments();Acceptance Criteria
- Comment DTO with all properties
- Reaction DTO with type constants
- CommentManager for CRUD operations
- CommentInstance for chainable actions
- Reaction management (create, list, delete)
- Timeline query support
- Event type filtering
- Full test coverage
- Follows established patterns
Dependencies
- Requires:
ConduitUI\Connector\GitHub - Requires:
ConduitUI\Issue\Data\User - Related to: Add Active Record entity layer (matching conduit-ui/pr pattern) #2 (IssueInstance integration)
Technical Notes
Reaction Types:
+1- Thumbs up-1- Thumbs downlaugh- Laughconfused- Confusedheart- Hearthooray- Hoorayrocket- Rocketeyes- Eyes
Timeline Event Types:
commented- Comment addedlabeled- Label addedunlabeled- Label removedassigned- User assignedunassigned- User unassignedclosed- Issue closedreopened- Issue reopenedreferenced- Referenced from commit/PRrenamed- Title changed- Many more...
GitHub API References:
coderabbitai
Metadata
Metadata
Assignees
Labels
No labels