Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/Contracts/IssuesServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

namespace ConduitUI\Issue\Contracts;

interface IssuesServiceInterface extends ManagesIssueAssigneesInterface, ManagesIssueLabelsInterface, ManagesIssuesInterface {}
interface IssuesServiceInterface extends ManagesIssueAssigneesInterface, ManagesIssueCommentsInterface, ManagesIssueLabelsInterface, ManagesIssuesInterface {}
34 changes: 34 additions & 0 deletions src/Contracts/ManagesIssueCommentsInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Contracts;

use ConduitUI\Issue\Data\Comment;
use ConduitUI\Issue\Data\Reaction;
use Illuminate\Support\Collection;

interface ManagesIssueCommentsInterface
{
/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Comment>
*/
public function listComments(string $owner, string $repo, int $issueNumber): Collection;

public function getComment(string $owner, string $repo, int $commentId): Comment;

public function createComment(string $owner, string $repo, int $issueNumber, string $body): Comment;

public function updateComment(string $owner, string $repo, int $commentId, string $body): Comment;

public function deleteComment(string $owner, string $repo, int $commentId): bool;

/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Reaction>
*/
public function listCommentReactions(string $owner, string $repo, int $commentId): Collection;

public function addCommentReaction(string $owner, string $repo, int $commentId, string $content): Reaction;

public function removeCommentReaction(string $owner, string $repo, int $commentId, int $reactionId): bool;
}
46 changes: 46 additions & 0 deletions src/Data/Comment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Data;

use DateTime;

readonly class Comment
{
public function __construct(
public int $id,
public string $body,
public User $user,
public DateTime $createdAt,
public DateTime $updatedAt,
public string $htmlUrl,
public string $issueUrl,
) {}

public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
body: $data['body'],
user: User::fromArray($data['user']),
createdAt: new DateTime($data['created_at']),
updatedAt: new DateTime($data['updated_at']),
htmlUrl: $data['html_url'],
issueUrl: $data['issue_url'],
);
}

public function toArray(): array
{
return [
'id' => $this->id,
'body' => $this->body,
'user' => $this->user->toArray(),
'created_at' => $this->createdAt->format('c'),
'updated_at' => $this->updatedAt->format('c'),
'html_url' => $this->htmlUrl,
'issue_url' => $this->issueUrl,
];
}
}
37 changes: 37 additions & 0 deletions src/Data/Reaction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Data;

use DateTime;

readonly class Reaction
{
public function __construct(
public int $id,
public string $content,
public User $user,
public DateTime $createdAt,
) {}

public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
content: $data['content'],
user: User::fromArray($data['user']),
createdAt: new DateTime($data['created_at']),
);
}

public function toArray(): array
{
return [
'id' => $this->id,
'content' => $this->content,
'user' => $this->user->toArray(),
'created_at' => $this->createdAt->format('c'),
];
}
}
2 changes: 2 additions & 0 deletions src/Services/IssuesService.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php

declare(strict_types=1);
Expand All @@ -7,12 +7,14 @@
use ConduitUi\GitHubConnector\Connector;
use ConduitUI\Issue\Contracts\IssuesServiceInterface;
use ConduitUI\Issue\Traits\ManagesIssueAssignees;
use ConduitUI\Issue\Traits\ManagesIssueComments;
use ConduitUI\Issue\Traits\ManagesIssueLabels;
use ConduitUI\Issue\Traits\ManagesIssues;

class IssuesService implements IssuesServiceInterface
{
use ManagesIssueAssignees;
use ManagesIssueComments;
use ManagesIssueLabels;
use ManagesIssues;

Expand Down
98 changes: 98 additions & 0 deletions src/Traits/ManagesIssueComments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Traits;

use ConduitUI\Issue\Data\Comment;
use ConduitUI\Issue\Data\Reaction;
use Illuminate\Support\Collection;

trait ManagesIssueComments
{
/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Comment>
*/
public function listComments(string $owner, string $repo, int $issueNumber): Collection
{
$response = $this->connector->send(
$this->connector->get("/repos/{$owner}/{$repo}/issues/{$issueNumber}/comments")
);

return collect($response->json())
->map(fn (array $data) => Comment::fromArray($data));
}
Comment on lines +13 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub REST API "List issue comments" endpoint per_page page pagination parameters

💡 Result:

  • Endpoint: GET /repos/{owner}/{repo}/issues/{issue_number}/comments. [1]

  • Pagination query params:

    • per_page — number of items per page. Default: 30. Maximum: 100. [1][2]
    • page — page number to fetch. Default: 1. [1][2]
  • How to iterate: responses include a Link header (rel="next", rel="last", etc.) to follow additional pages; you can also request specific pages via ?per_page=NN&page=MM. [2]

Example: GET https://api.github.com/repos/OWNER/REPO/issues/123/comments?per_page=100&page=2. [1][2]

Sources:
[1] GitHub REST API — List issue comments (issues/comments) (GitHub Docs).
[2] Using pagination in the REST API (GitHub Docs).


🏁 Script executed:

# Search for pagination patterns in the codebase
rg -i "per_page|pagination" --type php -A 2 -B 2

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

# Check the connector class to understand how it handles requests
fd connector -t f | head -20

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

# Look for other list methods in the trait to see if pagination is already used
ast-grep --pattern 'public function list$_($_) { $$$ }'

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

# Show fd connector output
fd connector -t f

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

# Show ast-grep output for list methods
ast-grep --pattern 'public function list$_($_) { $$$ }' -A 5

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

# Find and examine the connector class
fd -t f -i connector | grep -E '\.(php|ts|js)$'

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

# List all public methods in ManagesIssueComments trait
rg "public function" src/Traits/ManagesIssueComments.php

Repository: conduit-ui/issue

Length of output: 846


🏁 Script executed:

# Find where connector is defined/imported
rg "connector" src/Traits/ManagesIssueComments.php -B 5 -A 2

Repository: conduit-ui/issue

Length of output: 2847


🏁 Script executed:

# Search for connector class definition in the codebase
fd -type f -name "*onnector*.php"

Repository: conduit-ui/issue

Length of output: 230


🏁 Script executed:

# Check the class that uses this trait to understand connector origin
rg "ManagesIssueComments" --type php -B 5 -A 5

Repository: conduit-ui/issue

Length of output: 3253


🏁 Script executed:

# Look for composer.json to see dependencies
cat composer.json 2>/dev/null || echo "No composer.json found"

Repository: conduit-ui/issue

Length of output: 2747


List comments and list comment reactions need pagination support—consumers will silently miss data beyond the first page.

GitHub's "list issue comments" and "list comment reactions" endpoints are paginated with per_page (default: 30, max: 100) and page (default: 1) query parameters. Both listComments() and listCommentReactions() currently lack pagination parameters, so callers cannot request additional pages or customize page size.

Update both methods to accept pagination parameters, for example:

- public function listComments(string $owner, string $repo, int $issueNumber): Collection
+ public function listComments(string $owner, string $repo, int $issueNumber, int $perPage = 30, int $page = 1): Collection
  {
      $response = $this->connector->send(
-         $this->connector->get("/repos/{$owner}/{$repo}/issues/{$issueNumber}/comments")
+         $this->connector->get("/repos/{$owner}/{$repo}/issues/{$issueNumber}/comments", [
+             'per_page' => $perPage,
+             'page' => $page,
+         ])
      );
  }

Apply the same change to listCommentReactions(). Also update the ManagesIssueCommentsInterface contract to reflect these new parameters.

🤖 Prompt for AI Agents
In src/Traits/ManagesIssueComments.php around lines 13 to 24, listComments
currently fetches only the first page of issue comments; update it to accept
optional pagination parameters (int $page = 1, int $perPage = 30) and pass them
as query params to the connector (e.g. ?page=...&per_page=...), ensuring
per_page max handling can be left to caller or validated; apply the same change
to listCommentReactions() (add same signature and forward pagination query
params) and update the ManagesIssueCommentsInterface contract to declare the new
parameters and return type unchanged.


public function getComment(string $owner, string $repo, int $commentId): Comment
{
$response = $this->connector->send(
$this->connector->get("/repos/{$owner}/{$repo}/issues/comments/{$commentId}")
);

return Comment::fromArray($response->json());
}

public function createComment(string $owner, string $repo, int $issueNumber, string $body): Comment
{
$response = $this->connector->send(
$this->connector->post("/repos/{$owner}/{$repo}/issues/{$issueNumber}/comments", [
'body' => $body,
])
);

return Comment::fromArray($response->json());
}

public function updateComment(string $owner, string $repo, int $commentId, string $body): Comment
{
$response = $this->connector->send(
$this->connector->patch("/repos/{$owner}/{$repo}/issues/comments/{$commentId}", [
'body' => $body,
])
);

return Comment::fromArray($response->json());
}

public function deleteComment(string $owner, string $repo, int $commentId): bool
{
$response = $this->connector->send(
$this->connector->delete("/repos/{$owner}/{$repo}/issues/comments/{$commentId}")
);

return $response->successful();
}

/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Reaction>
*/
public function listCommentReactions(string $owner, string $repo, int $commentId): Collection
{
$response = $this->connector->send(
$this->connector->get("/repos/{$owner}/{$repo}/issues/comments/{$commentId}/reactions")
);

return collect($response->json())
->map(fn (array $data) => Reaction::fromArray($data));
}

public function addCommentReaction(string $owner, string $repo, int $commentId, string $content): Reaction
{
$response = $this->connector->send(
$this->connector->post("/repos/{$owner}/{$repo}/issues/comments/{$commentId}/reactions", [
'content' => $content,
])
);

return Reaction::fromArray($response->json());
}

public function removeCommentReaction(string $owner, string $repo, int $commentId, int $reactionId): bool
{
$response = $this->connector->send(
$this->connector->delete("/repos/{$owner}/{$repo}/issues/comments/{$commentId}/reactions/{$reactionId}")
);

return $response->successful();
}
}
56 changes: 56 additions & 0 deletions tests/Unit/Data/CommentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

use ConduitUI\Issue\Data\Comment;
use ConduitUI\Issue\Data\User;

test('can create comment from array', function () {
$data = [
'id' => 123,
'body' => 'This is a test comment',
'user' => [
'id' => 456,
'login' => 'testuser',
'avatar_url' => 'https://github.com/testuser.png',
'html_url' => 'https://github.com/testuser',
'type' => 'User',
],
'created_at' => '2023-01-01T12:00:00Z',
'updated_at' => '2023-01-02T12:00:00Z',
'html_url' => 'https://github.com/owner/repo/issues/comments/123',
'issue_url' => 'https://api.github.com/repos/owner/repo/issues/1',
];

$comment = Comment::fromArray($data);

expect($comment->id)->toBe(123);
expect($comment->body)->toBe('This is a test comment');
expect($comment->user)->toBeInstanceOf(User::class);
expect($comment->user->login)->toBe('testuser');
expect($comment->htmlUrl)->toBe('https://github.com/owner/repo/issues/comments/123');
expect($comment->issueUrl)->toBe('https://api.github.com/repos/owner/repo/issues/1');
});

test('can convert comment to array', function () {
$user = new User(456, 'testuser', 'https://github.com/testuser.png', 'https://github.com/testuser', 'User');

$comment = new Comment(
id: 123,
body: 'This is a test comment',
user: $user,
createdAt: new DateTime('2023-01-01T12:00:00Z'),
updatedAt: new DateTime('2023-01-02T12:00:00Z'),
htmlUrl: 'https://github.com/owner/repo/issues/comments/123',
issueUrl: 'https://api.github.com/repos/owner/repo/issues/1',
);

$array = $comment->toArray();

expect($array['id'])->toBe(123);
expect($array['body'])->toBe('This is a test comment');
expect($array['user'])->toBeArray();
expect($array['user']['login'])->toBe('testuser');
expect($array['html_url'])->toBe('https://github.com/owner/repo/issues/comments/123');
expect($array['issue_url'])->toBe('https://api.github.com/repos/owner/repo/issues/1');
});
68 changes: 68 additions & 0 deletions tests/Unit/Data/ReactionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

use ConduitUI\Issue\Data\Reaction;
use ConduitUI\Issue\Data\User;

test('can create reaction from array', function () {
$data = [
'id' => 123,
'content' => '+1',
'user' => [
'id' => 456,
'login' => 'testuser',
'avatar_url' => 'https://github.com/testuser.png',
'html_url' => 'https://github.com/testuser',
'type' => 'User',
],
'created_at' => '2023-01-01T12:00:00Z',
];

$reaction = Reaction::fromArray($data);

expect($reaction->id)->toBe(123);
expect($reaction->content)->toBe('+1');
expect($reaction->user)->toBeInstanceOf(User::class);
expect($reaction->user->login)->toBe('testuser');
});

test('can convert reaction to array', function () {
$user = new User(456, 'testuser', 'https://github.com/testuser.png', 'https://github.com/testuser', 'User');

$reaction = new Reaction(
id: 123,
content: '+1',
user: $user,
createdAt: new DateTime('2023-01-01T12:00:00Z'),
);

$array = $reaction->toArray();

expect($array['id'])->toBe(123);
expect($array['content'])->toBe('+1');
expect($array['user'])->toBeArray();
expect($array['user']['login'])->toBe('testuser');
});

test('supports various reaction types', function (string $content) {
$user = new User(456, 'testuser', 'https://github.com/testuser.png', 'https://github.com/testuser', 'User');

$reaction = new Reaction(
id: 123,
content: $content,
user: $user,
createdAt: new DateTime('2023-01-01T12:00:00Z'),
);

expect($reaction->content)->toBe($content);
})->with([
'+1',
'-1',
'laugh',
'confused',
'heart',
'hooray',
'rocket',
'eyes',
]);
Loading