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, ManagesIssueLabelsInterface, ManagesIssuesInterface, ManagesMilestonesInterface {}
34 changes: 34 additions & 0 deletions src/Contracts/ManagesMilestonesInterface.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\Issue;
use ConduitUI\Issue\Data\Milestone;
use Illuminate\Support\Collection;

interface ManagesMilestonesInterface
{
/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Milestone>
*/
public function listMilestones(string $owner, string $repo, array $filters = []): Collection;

public function getMilestone(string $owner, string $repo, int $milestoneNumber): Milestone;

public function createMilestone(string $owner, string $repo, array $data): Milestone;

public function updateMilestone(string $owner, string $repo, int $milestoneNumber, array $data): Milestone;

public function deleteMilestone(string $owner, string $repo, int $milestoneNumber): void;

public function assignIssueToMilestone(string $owner, string $repo, int $issueNumber, int $milestoneNumber): Issue;

public function removeIssueFromMilestone(string $owner, string $repo, int $issueNumber): Issue;

/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Issue>
*/
public function listMilestoneIssues(string $owner, string $repo, int $milestoneNumber, array $filters = []): Collection;
}
6 changes: 3 additions & 3 deletions src/Data/Issue.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public function __construct(
public bool $locked,
public array $assignees,
public array $labels,
public ?string $milestone,
public ?Milestone $milestone,
public int $comments,
public DateTime $createdAt,
public DateTime $updatedAt,
Expand All @@ -39,7 +39,7 @@ public static function fromArray(array $data): self
locked: $data['locked'],
assignees: array_map(fn ($assignee) => User::fromArray($assignee), $data['assignees'] ?? []),
labels: array_map(fn ($label) => Label::fromArray($label), $data['labels'] ?? []),
milestone: $data['milestone']['title'] ?? null,
milestone: $data['milestone'] ? Milestone::fromArray($data['milestone']) : null,
comments: $data['comments'],
createdAt: new DateTime($data['created_at']),
updatedAt: new DateTime($data['updated_at']),
Expand All @@ -62,7 +62,7 @@ public function toArray(): array
'locked' => $this->locked,
'assignees' => array_map(fn (User $assignee) => $assignee->toArray(), $this->assignees),
'labels' => array_map(fn (Label $label) => $label->toArray(), $this->labels),
'milestone' => $this->milestone,
'milestone' => $this->milestone?->toArray(),
'comments' => $this->comments,
'created_at' => $this->createdAt->format('c'),
'updated_at' => $this->updatedAt->format('c'),
Expand Down
74 changes: 74 additions & 0 deletions src/Data/Milestone.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Data;

use DateTime;

readonly class Milestone
{
public function __construct(
public int $id,
public int $number,
public string $title,
public ?string $description,
public string $state,
public int $openIssues,
public int $closedIssues,
public DateTime $createdAt,
public DateTime $updatedAt,
public ?DateTime $closedAt,
public ?DateTime $dueOn,
public string $htmlUrl,
public User $creator,
) {}

public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
number: $data['number'],
title: $data['title'],
description: $data['description'] ?? null,
state: $data['state'],
openIssues: $data['open_issues'],
closedIssues: $data['closed_issues'],
createdAt: new DateTime($data['created_at']),
updatedAt: new DateTime($data['updated_at']),
closedAt: $data['closed_at'] ? new DateTime($data['closed_at']) : null,
dueOn: $data['due_on'] ? new DateTime($data['due_on']) : null,
htmlUrl: $data['html_url'],
creator: User::fromArray($data['creator']),
);
}
Comment on lines +27 to +44
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

Harden fromArray() against missing closed_at / due_on keys.

-            closedAt: $data['closed_at'] ? new DateTime($data['closed_at']) : null,
-            dueOn: $data['due_on'] ? new DateTime($data['due_on']) : null,
+            closedAt: (isset($data['closed_at']) && $data['closed_at'] !== null) ? new DateTime($data['closed_at']) : null,
+            dueOn: (isset($data['due_on']) && $data['due_on'] !== null) ? new DateTime($data['due_on']) : null,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
number: $data['number'],
title: $data['title'],
description: $data['description'] ?? null,
state: $data['state'],
openIssues: $data['open_issues'],
closedIssues: $data['closed_issues'],
createdAt: new DateTime($data['created_at']),
updatedAt: new DateTime($data['updated_at']),
closedAt: $data['closed_at'] ? new DateTime($data['closed_at']) : null,
dueOn: $data['due_on'] ? new DateTime($data['due_on']) : null,
htmlUrl: $data['html_url'],
creator: User::fromArray($data['creator']),
);
}
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
number: $data['number'],
title: $data['title'],
description: $data['description'] ?? null,
state: $data['state'],
openIssues: $data['open_issues'],
closedIssues: $data['closed_issues'],
createdAt: new DateTime($data['created_at']),
updatedAt: new DateTime($data['updated_at']),
closedAt: (isset($data['closed_at']) && $data['closed_at'] !== null) ? new DateTime($data['closed_at']) : null,
dueOn: (isset($data['due_on']) && $data['due_on'] !== null) ? new DateTime($data['due_on']) : null,
htmlUrl: $data['html_url'],
creator: User::fromArray($data['creator']),
);
}
🤖 Prompt for AI Agents
In src/Data/Milestone.php around lines 27 to 44, the fromArray() method assumes
'closed_at' and 'due_on' keys always exist; update it to first check those keys
(use array_key_exists or isset) and that their values are non-empty before
constructing DateTime, otherwise pass null. Replace the current ternary
expressions with checks like: if key exists and not null/empty then new
DateTime(value) else null; keep other fields unchanged.


public function toArray(): array
{
return [
'id' => $this->id,
'number' => $this->number,
'title' => $this->title,
'description' => $this->description,
'state' => $this->state,
'open_issues' => $this->openIssues,
'closed_issues' => $this->closedIssues,
'created_at' => $this->createdAt->format('c'),
'updated_at' => $this->updatedAt->format('c'),
'closed_at' => $this->closedAt?->format('c'),
'due_on' => $this->dueOn?->format('c'),
'html_url' => $this->htmlUrl,
'creator' => $this->creator->toArray(),
];
}

public function isOpen(): bool
{
return $this->state === 'open';
}

public function isClosed(): bool
{
return $this->state === 'closed';
}
}
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 @@ -9,12 +9,14 @@
use ConduitUI\Issue\Traits\ManagesIssueAssignees;
use ConduitUI\Issue\Traits\ManagesIssueLabels;
use ConduitUI\Issue\Traits\ManagesIssues;
use ConduitUI\Issue\Traits\ManagesMilestones;

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

public function __construct(
private readonly Connector $connector
Expand Down
79 changes: 79 additions & 0 deletions src/Traits/ManagesMilestones.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Traits;

use ConduitUI\Issue\Data\Issue;
use ConduitUI\Issue\Data\Milestone;
use Illuminate\Support\Collection;

trait ManagesMilestones
{
/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Milestone>
*/
public function listMilestones(string $owner, string $repo, array $filters = []): Collection
{
$response = $this->connector->send(
$this->connector->get("/repos/{$owner}/{$repo}/milestones", $filters)
);

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

public function getMilestone(string $owner, string $repo, int $milestoneNumber): Milestone
{
$response = $this->connector->send(
$this->connector->get("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}")
);

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

public function createMilestone(string $owner, string $repo, array $data): Milestone
{
$response = $this->connector->send(
$this->connector->post("/repos/{$owner}/{$repo}/milestones", $data)
);

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

public function updateMilestone(string $owner, string $repo, int $milestoneNumber, array $data): Milestone
{
$response = $this->connector->send(
$this->connector->patch("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}", $data)
);

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

public function deleteMilestone(string $owner, string $repo, int $milestoneNumber): void
{
$this->connector->send(
$this->connector->delete("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}")
);
}
Comment on lines +16 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.php" | grep -i connector | head -20

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

find . -type f -name "*.php" -path "*/src/*" | head -30

Repository: conduit-ui/issue

Length of output: 621


🏁 Script executed:

git ls-files "*.php" | grep -E "(Connector|connector)" | head -20

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

cat -n src/Services/IssuesService.php | head -100

Repository: conduit-ui/issue

Length of output: 834


🏁 Script executed:

cat -n src/Traits/ManagesIssues.php | head -150

Repository: conduit-ui/issue

Length of output: 2340


🏁 Script executed:

find . -type f -name "composer.json" -o -name "composer.lock" | head -5

Repository: conduit-ui/issue

Length of output: 91


🏁 Script executed:

cat composer.json | grep -A 5 -B 5 "GitHubConnector" 2>/dev/null || cat composer.json | head -100

Repository: conduit-ui/issue

Length of output: 2747


🏁 Script executed:

find . -path "*/vendor/*" -name "Connector.php" 2>/dev/null | grep -i github | head -5

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

find . -path "*/vendor/conduit-ui/*" -type f -name "*.php" 2>/dev/null | head -20

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

rg "class Connector" -A 20 --type php

Repository: conduit-ui/issue

Length of output: 42


🏁 Script executed:

find ./tests -type f -name "*.php" 2>/dev/null | head -10

Repository: conduit-ui/issue

Length of output: 228


🏁 Script executed:

rg "listMilestones|getMilestone" -A 10 --type php

Repository: conduit-ui/issue

Length of output: 2820


🌐 Web query:

conduit-ui/connector GitHub repository

💡 Result:

I couldn't find a GitHub repository named exactly "conduit-ui/connector". Related projects you might mean:

  • conduit-ui/conduit (Conduit CLI platform) — package info and source reference. [1]
  • conduit-ui/github-connector (a GitHub API connector package from conduit-ui on Packagist/Libraries.io). [1][2]
  • ConduitPlatform/conduit-ui (a separate Conduit web UI repo / project surfaced by mirrors/analysis tools). [3]

If you want, tell me which one you meant (or paste a link) and I’ll open it and summarize the repo contents.

Sources: [1] Packagist conduit-ui listings; [2] Libraries.io entry for conduit-ui/github-connector; [3] Conduit-UI code listing (mirror/analysis).


🌐 Web query:

Saloon PHP HTTP client URL path parameter encoding

💡 Result:

Short answer: Saloon does not automatically percent‑encode URL path segments for you — you should encode path parameters yourself before returning the endpoint in resolveEndpoint. Use rawurlencode (RFC‑3986) per segment (or a URI library like League\Uri\Encoder::encodePath if you need finer control) and then build the endpoint string. [1][2][3]

Example (concept):
$segment = rawurlencode($value);
return "/items/{$segment}/details";

Sources:
[1] Saloon — Requests / resolveEndpoint (docs). (docs.saloon.dev)
[2] PHP rawurlencode() manual (RFC‑3986 behavior). (php.net)
[3] League\Uri Encoder — encodePath utility. (uri.thephpleague.com)

Citations:


🌐 Web query:

site:packagist.org conduit-ui/connector

💡 Result:

I couldn't find a Packagist package named "conduit-ui/connector". The closest match is "conduit-ui/conduit" on Packagist. [1]

Sources
[1] conduit-ui/conduit — Packagist.


🏁 Script executed:

cat -n tests/TestCase.php

Repository: conduit-ui/issue

Length of output: 924


🏁 Script executed:

cat -n src/Traits/ManagesMilestones.php

Repository: conduit-ui/issue

Length of output: 3235


Encode $owner and $repo path parameters with rawurlencode().

Saloon (the underlying HTTP client) does not automatically percent-encode URL path segments. If these parameters contain special characters (including /), the endpoint will be malformed. Add rawurlencode() to all methods that interpolate these values:

     public function listMilestones(string $owner, string $repo, array $filters = []): Collection
     {
+        $owner = rawurlencode($owner);
+        $repo = rawurlencode($repo);
         $response = $this->connector->send(
             $this->connector->get("/repos/{$owner}/{$repo}/milestones", $filters)
         );
@@
     public function getMilestone(string $owner, string $repo, int $milestoneNumber): Milestone
     {
+        $owner = rawurlencode($owner);
+        $repo = rawurlencode($repo);
         $response = $this->connector->send(
             $this->connector->get("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}")
         );
@@
     public function createMilestone(string $owner, string $repo, array $data): Milestone
     {
+        $owner = rawurlencode($owner);
+        $repo = rawurlencode($repo);
         $response = $this->connector->send(
             $this->connector->post("/repos/{$owner}/{$repo}/milestones", $data)
         );
@@
     public function updateMilestone(string $owner, string $repo, int $milestoneNumber, array $data): Milestone
     {
+        $owner = rawurlencode($owner);
+        $repo = rawurlencode($repo);
         $response = $this->connector->send(
             $this->connector->patch("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}", $data)
         );
@@
     public function deleteMilestone(string $owner, string $repo, int $milestoneNumber): void
     {
+        $owner = rawurlencode($owner);
+        $repo = rawurlencode($repo);
         $this->connector->send(
             $this->connector->delete("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}")
         );
     }

Note: This same pattern appears in ManagesIssues and other traits and should be fixed consistently across the codebase.

🤖 Prompt for AI Agents
In src/Traits/ManagesMilestones.php around lines 16 to 58, path parameters
$owner and $repo are interpolated directly into URLs and must be
percent-encoded; update every method that builds a repo path (listMilestones,
getMilestone, createMilestone, updateMilestone, deleteMilestone) to wrap $owner
and $repo with rawurlencode() when constructing the endpoint strings so special
characters (including "/") are encoded, and apply the same change to other
traits like ManagesIssues that use the same pattern to ensure consistent, safe
URL construction across the codebase.


public function assignIssueToMilestone(string $owner, string $repo, int $issueNumber, int $milestoneNumber): Issue
{
return $this->updateIssue($owner, $repo, $issueNumber, ['milestone' => $milestoneNumber]);
}

public function removeIssueFromMilestone(string $owner, string $repo, int $issueNumber): Issue
{
return $this->updateIssue($owner, $repo, $issueNumber, ['milestone' => null]);
}

/**
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Issue>
*/
public function listMilestoneIssues(string $owner, string $repo, int $milestoneNumber, array $filters = []): Collection
{
$filters['milestone'] = (string) $milestoneNumber;

return $this->listIssues($owner, $repo, $filters);
}
}
43 changes: 40 additions & 3 deletions tests/Unit/Data/IssueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use ConduitUI\Issue\Data\Issue;
use ConduitUI\Issue\Data\Label;
use ConduitUI\Issue\Data\Milestone;
use ConduitUI\Issue\Data\User;

test('can create issue from array', function () {
Expand Down Expand Up @@ -32,7 +33,25 @@
],
],
'milestone' => [
'id' => 999,
'number' => 1,
'title' => 'v1.0',
'description' => 'Version 1.0',
'state' => 'open',
'open_issues' => 5,
'closed_issues' => 3,
'created_at' => '2023-01-01T12:00:00Z',
'updated_at' => '2023-01-02T12:00:00Z',
'closed_at' => null,
'due_on' => null,
'html_url' => 'https://github.com/owner/repo/milestone/1',
'creator' => [
'id' => 201,
'login' => 'creator',
'avatar_url' => 'https://github.com/creator.png',
'html_url' => 'https://github.com/creator',
'type' => 'User',
],
],
'comments' => 5,
'created_at' => '2023-01-01T12:00:00Z',
Expand Down Expand Up @@ -68,7 +87,8 @@
expect($issue->assignees[0])->toBeInstanceOf(User::class);
expect($issue->labels)->toHaveCount(1);
expect($issue->labels[0])->toBeInstanceOf(Label::class);
expect($issue->milestone)->toBe('v1.0');
expect($issue->milestone)->toBeInstanceOf(Milestone::class);
expect($issue->milestone->title)->toBe('v1.0');
expect($issue->comments)->toBe(5);
expect($issue->user)->toBeInstanceOf(User::class);
expect($issue->assignee)->toBeInstanceOf(User::class);
Expand All @@ -79,6 +99,22 @@
$user = new User(101, 'author', 'https://github.com/author.png', 'https://github.com/author', 'User');
$assignee = new User(456, 'testuser', 'https://github.com/testuser.png', 'https://github.com/testuser', 'User');
$label = new Label(789, 'bug', 'fc2929', 'Something is broken');
$creator = new User(201, 'creator', 'https://github.com/creator.png', 'https://github.com/creator', 'User');
$milestone = new Milestone(
id: 999,
number: 1,
title: 'v1.0',
description: 'Version 1.0',
state: 'open',
openIssues: 5,
closedIssues: 3,
createdAt: new DateTime('2023-01-01T12:00:00Z'),
updatedAt: new DateTime('2023-01-02T12:00:00Z'),
closedAt: null,
dueOn: null,
htmlUrl: 'https://github.com/owner/repo/milestone/1',
creator: $creator,
);

$issue = new Issue(
id: 123,
Expand All @@ -89,7 +125,7 @@
locked: false,
assignees: [$assignee],
labels: [$label],
milestone: 'v1.0',
milestone: $milestone,
comments: 5,
createdAt: new DateTime('2023-01-01T12:00:00Z'),
updatedAt: new DateTime('2023-01-02T12:00:00Z'),
Expand All @@ -108,7 +144,8 @@
expect($array['state'])->toBe('open');
expect($array['assignees'])->toHaveCount(1);
expect($array['labels'])->toHaveCount(1);
expect($array['milestone'])->toBe('v1.0');
expect($array['milestone'])->toBeArray();
expect($array['milestone']['title'])->toBe('v1.0');
expect($array['closed_at'])->toBeNull();
});

Expand Down
Loading
Loading