Skip to content

Facade: Static PullRequests facade for convenient access #28

@jordanpartridge

Description

@jordanpartridge

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

  1. Ergonomic API: Clean, expressive static access
  2. IDE Support: Full autocomplete via PHPDoc annotations
  3. Discoverability: Easy to find all available methods
  4. Consistency: Matches Laravel's facade pattern
  5. Flexibility: Supports both static and instance-based usage

Related Issues

References

Labels

  • enhancement
  • facade
  • api
  • dx

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