Summary
Create Laravel facade providing static access to the PR management API, following the pattern established in conduit-ui/repo.
Acceptance Criteria
Facade Class
namespace ConduitUI\Pr\Facades;
use ConduitUI\Pr\Contracts\PullRequestBuilderInterface;
use ConduitUI\Pr\Contracts\PullRequestQueryInterface;
use ConduitUI\Pr\Data\PullRequest;
use ConduitUI\Pr\Services\PullRequests as PullRequestsService;
use Illuminate\Support\Facades\Facade;
/**
* @method static PullRequestInstance find(string $fullName, int $number)
* @method static PullRequest get(string $fullName, int $number)
* @method static PullRequestQueryInterface forRepo(string $fullName)
* @method static PullRequestBuilderInterface create(string $fullName)
* @method static PullRequestQueryInterface query(string $fullName)
*
* @see PullRequestsService
*/
final class PullRequests extends Facade
{
protected static function getFacadeAccessor(): string
{
return PullRequestsService::class;
}
}
Service Implementation
namespace ConduitUI\Pr\Services;
use ConduitUI\Connector\GitHub;
use ConduitUI\Pr\Contracts\PullRequestBuilderInterface;
use ConduitUI\Pr\Contracts\PullRequestQueryInterface;
use ConduitUI\Pr\Data\PullRequest;
final class PullRequests
{
public function __construct(
protected GitHub $github,
) {}
/**
* Get a pull request instance for chaining operations
*/
public function find(string $fullName, int $number): PullRequestInstance
{
return new PullRequestInstance($this->github, $fullName, $number);
}
/**
* Get pull request data directly
*/
public function get(string $fullName, int $number): PullRequest
{
$response = $this->github->get("/repos/{$fullName}/pulls/{$number}");
return PullRequest::fromArray($response->json());
}
/**
* Start a query for a specific repository
*/
public function forRepo(string $fullName): PullRequestQuery
{
return new PullRequestQuery($this->github, $fullName);
}
/**
* Alias for forRepo
*/
public function query(string $fullName): PullRequestQuery
{
return $this->forRepo($fullName);
}
/**
* Create a new pull request
*/
public function create(string $fullName): PullRequestBuilder
{
return new PullRequestBuilder($this->github, $fullName);
}
}
PullRequestInstance Class
namespace ConduitUI\Pr\Services;
use ConduitUI\Connector\GitHub;
use ConduitUI\Pr\Data\PullRequest;
/**
* Represents a single PR with chainable operations
*/
final class PullRequestInstance
{
protected ?PullRequest $pullRequest = null;
public function __construct(
protected GitHub $github,
protected string $fullName,
protected int $number,
) {}
/**
* Get the PR data (cached)
*/
public function get(): PullRequest
{
if ($this->pullRequest === null) {
$this->pullRequest = $this->fetch();
}
return $this->pullRequest;
}
/**
* Fetch fresh PR data
*/
public function fresh(): PullRequest
{
$this->pullRequest = $this->fetch();
return $this->pullRequest;
}
/**
* Fetch PR from GitHub API
*/
protected function fetch(): PullRequest
{
$response = $this->github->get(
"/repos/{$this->fullName}/pulls/{$this->number}"
);
return PullRequest::fromArray($response->json());
}
/**
* Access reviews for this PR
*/
public function reviews(): ReviewQuery
{
return new ReviewQuery($this->github, $this->fullName, $this->number);
}
/**
* Access checks for this PR
*/
public function checks(): CheckRunQuery
{
$pr = $this->get();
return new CheckRunQuery($this->github, $this->fullName, $pr->headSha);
}
/**
* Access files for this PR
*/
public function files(): FileQuery
{
return new FileQuery($this->github, $this->fullName, $this->number);
}
/**
* Access comments for this PR
*/
public function comments(): CommentManager
{
return new CommentManager($this->github, $this->fullName, $this->number);
}
// State actions
public function close(string $comment = null): PullRequest
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->close($comment);
}
public function reopen(): PullRequest
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->reopen();
}
public function markDraft(): PullRequest
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->markDraft();
}
public function markReady(): PullRequest
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->markReady();
}
// Review actions
public function approve(string $comment = null): ReviewBuilder
{
return (new ReviewBuilder($this->github, $this->fullName, $this->number))
->approve($comment);
}
public function requestChanges(string $comment = null): ReviewBuilder
{
return (new ReviewBuilder($this->github, $this->fullName, $this->number))
->requestChanges($comment);
}
public function comment(string $body): PullRequest
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->comment($body);
}
// Label actions
public function addLabel(string $label): PullRequestActions
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->addLabel($label);
}
public function addLabels(array $labels): PullRequestActions
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->addLabels($labels);
}
// Reviewer actions
public function requestReview(string $username): PullRequestActions
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->requestReview($username);
}
public function requestReviews(array $usernames): PullRequestActions
{
return (new PullRequestActions($this->github, $this->fullName, $this->number))
->requestReviews($usernames);
}
// Merge actions
public function merge(
string $strategy = 'merge',
?string $title = null,
?string $message = null
): PullRequest {
$manager = new MergeManager($this->github, $this->fullName, $this->number);
return match($strategy) {
'squash' => $manager->squash($title, $message),
'rebase' => $manager->rebase(),
default => $manager->merge($title, $message),
};
}
public function squashMerge(?string $title = null, ?string $message = null): PullRequest
{
return (new MergeManager($this->github, $this->fullName, $this->number))
->squash($title, $message);
}
public function rebaseMerge(): PullRequest
{
return (new MergeManager($this->github, $this->fullName, $this->number))
->rebase();
}
public function deleteBranch(): bool
{
return (new MergeManager($this->github, $this->fullName, $this->number))
->deleteBranch();
}
// Helpers
public function checksPass(): bool
{
return $this->checks()->summary()->allPassing();
}
public function checksFail(): bool
{
return $this->checks()->summary()->anyFailing();
}
public function isApproved(): bool
{
return $this->reviews()->whereApproved()->isNotEmpty();
}
// Magic property access
public function __get(string $name): mixed
{
return $this->get()->$name;
}
public function __isset(string $name): bool
{
return isset($this->get()->$name);
}
}
Service Provider Registration
namespace ConduitUI\Pr;
use Illuminate\Support\ServiceProvider;
use ConduitUI\Pr\Services\PullRequests;
class PrServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PullRequests::class, function ($app) {
return new PullRequests($app->make(GitHub::class));
});
// Register all other bindings
}
}
Usage Examples
Static Facade Access
use ConduitUI\Pr\Facades\PullRequests;
// Find and operate on PR
PullRequests::find('owner/repo', 123)->approve('LGTM')->submit();
// Get PR data
$pr = PullRequests::get('owner/repo', 123);
// Query PRs
$openPrs = PullRequests::forRepo('owner/repo')
->whereOpen()
->get();
// Create PR
$pr = PullRequests::create('owner/repo')
->title('feat: New feature')
->head('feature-branch')
->create();
Instance-Based Access
use ConduitUI\Pr\Services\PullRequests;
$prs = app(PullRequests::class);
// Same API
$prs->find('owner/repo', 123)->approve('LGTM')->submit();
Chaining Operations
// The instance pattern enables natural chaining
PullRequests::find('owner/repo', 123)
->addLabel('ready-for-review')
->requestReview('senior-dev')
->comment('Please review');
// Access sub-queries
$reviews = PullRequests::find('owner/repo', 123)->reviews()->get();
$checks = PullRequests::find('owner/repo', 123)->checks()->whereFailing();
$files = PullRequests::find('owner/repo', 123)->files()->whereExtension('php');
Property Access
$pr = PullRequests::find('owner/repo', 123);
// Magic property access delegates to DTO
echo $pr->title;
echo $pr->state;
echo $pr->author->login;
Testing Requirements
it('provides static facade access')
->expect(fn() => PullRequests::find('test/repo', 1))
->toBeInstanceOf(PullRequestInstance::class);
it('supports forRepo query builder')
->expect(fn() => PullRequests::forRepo('test/repo'))
->toBeInstanceOf(PullRequestQuery::class);
it('supports PR creation')
->expect(fn() => PullRequests::create('test/repo'))
->toBeInstanceOf(PullRequestBuilder::class);
it('chains operations')
->expect(fn() =>
PullRequests::find('test/repo', 1)
->addLabel('test')
->requestReview('user')
)->toBeInstanceOf(PullRequestActions::class);
it('accesses magic properties')
->expect(fn() => PullRequests::find('test/repo', 1)->title)
->toBeString();
Benefits
- Ergonomic API: Clean, expressive static access
- IDE Support: Full autocomplete via PHPDoc annotations
- Discoverability: Easy to find all available methods
- Consistency: Matches Laravel's facade pattern
- Flexibility: Supports both static and instance-based usage
Related Issues
References
Labels
- enhancement
- facade
- api
- dx
Summary
Create Laravel facade providing static access to the PR management API, following the pattern established in conduit-ui/repo.
Acceptance Criteria
Facade Class
Service Implementation
PullRequestInstance Class
Service Provider Registration
Usage Examples
Static Facade Access
Instance-Based Access
Chaining Operations
Property Access
Testing Requirements
Benefits
Related Issues
References
Labels