Skip to content

Facade: Static PullRequests facade for convenient access #28

Closed

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

Activity

  1. coderabbitai commented on Dec 19, 2025

    @coderabbitai

    📝 CodeRabbit Plan Mode

    Generate an implementation plan and prompts that you can use with your favorite coding agent.

    • Create Plan
    Examples

    🔗 Similar Issues

    Possible Duplicates

    Related Issues

    🔗 Related PRs

    conduit-ui/conduit#48 - 🔒 SECURITY: Make Conduit Great Again - CodeRabbit Security Revolution [merged]
    conduit-ui/conduit#66 - 🚀 FEATURE: Complete PR CRUD System - Parity with Issues [merged]
    conduit-ui/conduit#72 - 🔧 FIX: Proper handling of closed/merged PRs in status command [merged]
    #6 - Add Saloon Request classes, service layer, and pagination [merged]
    #17 - Define key agent interfaces for PRs and issues [merged]

    👤 Suggested Assignees

    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord or schedule a call!

  2. jordanpartridge commented on May 19, 2026

    @jordanpartridge
    ContributorAuthor

    Implemented and merged via #37. src/Facades/PullRequests.php plus the static factory in src/PullRequests.php (with setConnector(), for(), find(), create(), query()) provide the facade entry point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions