Skip to content

Data: PullRequest DTO with chainable actions #21

@jordanpartridge

Description

@jordanpartridge

Summary

Create strongly-typed PullRequest data transfer object with chainable action methods for individual PR operations.

Acceptance Criteria

DTO Properties

namespace ConduitUI\Pr\Data;

use Carbon\Carbon;
use Illuminate\Support\Collection;

final readonly class PullRequest
{
    public function __construct(
        public int $id,
        public int $number,
        public string $title,
        public string $body,
        public string $state, // 'open' | 'closed'
        public bool $draft,
        public bool $merged,
        public ?Carbon $mergedAt,
        public User $author,
        public string $headBranch,
        public string $headRef,
        public string $headSha,
        public string $baseBranch,
        public string $baseRef,
        public string $baseSha,
        public bool $mergeable,
        public ?string $mergeableState,
        public int $comments,
        public int $reviewComments,
        public int $commits,
        public int $additions,
        public int $deletions,
        public int $changedFiles,
        public Collection $labels, // Collection<Label>
        public Collection $assignees, // Collection<User>
        public Collection $requestedReviewers, // Collection<User>
        public string $htmlUrl,
        public string $diffUrl,
        public string $patchUrl,
        public Carbon $createdAt,
        public Carbon $updatedAt,
        public ?Carbon $closedAt,
    ) {}
    
    public static function fromArray(array $data): self;
}

Chainable Action Methods

The DTO should return action builder instances for chainable operations:

// State changes
public function close(string $comment = null): PullRequestActions;
public function reopen(): PullRequestActions;
public function markDraft(): PullRequestActions;
public function markReady(): PullRequestActions;

// Reviews
public function approve(string $comment = null): ReviewBuilder;
public function requestChanges(string $comment = null): ReviewBuilder;
public function comment(string $body): PullRequestActions;

// Labels
public function addLabel(string $label): PullRequestActions;
public function addLabels(array $labels): PullRequestActions;
public function removeLabel(string $label): PullRequestActions;
public function setLabels(array $labels): PullRequestActions;

// Reviewers
public function requestReview(string $username): PullRequestActions;
public function requestReviews(array $usernames): PullRequestActions;
public function requestTeamReview(string $teamSlug): PullRequestActions;

// Assignees
public function assign(string $username): PullRequestActions;
public function unassign(string $username): PullRequestActions;

// Merge operations
public function merge(string $strategy = 'merge', ?string $message = null): PullRequestActions;
public function squashMerge(?string $message = null): PullRequestActions;
public function rebaseMerge(): PullRequestActions;

// Queries
public function reviews(): ReviewQuery;
public function checks(): CheckRunQuery;
public function files(): FileQuery;
public function commits(): CommitQuery;

// Helpers
public function checksPass(): bool;
public function checksFail(): bool;
public function isApproved(): bool;
public function hasChangesRequested(): bool;
public function approvalCount(): int;

Related Data Objects

// User DTO
final readonly class User
{
    public function __construct(
        public int $id,
        public string $login,
        public string $type, // 'User' | 'Bot'
        public string $avatarUrl,
        public string $htmlUrl,
    ) {}
}

// Label DTO
final readonly class Label
{
    public function __construct(
        public int $id,
        public string $name,
        public string $color,
        public string $description,
    ) {}
}

Technical Implementation

Pattern: DTO returns action builders

// The DTO is readonly and immutable
// Actions return builder instances that execute operations

final readonly class PullRequest
{
    // ... properties
    
    public function approve(string $comment = null): ReviewBuilder
    {
        return (new ReviewBuilder($this->github, $this->fullName, $this->number))
            ->approve($comment);
    }
    
    public function merge(string $strategy = 'merge', ?string $message = null): PullRequestActions
    {
        return (new PullRequestActions($this->github, $this->fullName, $this->number))
            ->merge($strategy, $message);
    }
}

Usage Examples

$pr = PullRequests::find('owner/repo', 123);

// Access properties
echo $pr->title;
echo $pr->state;
echo $pr->author->login;

// Chain actions
$pr->approve('LGTM!')
   ->merge(strategy: 'squash');

// Query related data
$reviews = $pr->reviews()->get();
$checks = $pr->checks()->whereFailed()->get();

// Helpers
if ($pr->checksPass() && $pr->isApproved()) {
    $pr->merge();
}

Testing Requirements

it('creates PR from array')
    ->expect(fn() => PullRequest::fromArray($apiResponse))
    ->toBeInstanceOf(PullRequest::class);

it('has readonly properties')
    ->expect(fn() => $pr->title = 'new')
    ->toThrow(Error::class);

it('returns action builders')
    ->expect(fn() => $pr->approve())
    ->toBeInstanceOf(ReviewBuilder::class);

it('chains multiple actions')
    ->expect(fn() => 
        $pr->addLabel('ready')
           ->requestReview('senior-dev')
           ->comment('Ready for review')
    )->toBeInstanceOf(PullRequestActions::class);

Dependencies

  • conduit-ui/connector
  • Carbon for date handling
  • Laravel Collections

References

Labels

  • enhancement
  • core
  • data-objects

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions