Skip to content
Merged
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"conduit-ui/connector": "^1.0"
},
"require-dev": {
"illuminate/support": "^12.43",
"laravel/pint": "^1.0",
"pestphp/pest": "^3.0",
"phpstan/extension-installer": "*",
Expand Down
31 changes: 31 additions & 0 deletions src/Facades/PullRequests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Pr\Facades;

use ConduitUI\Pr\PullRequest;
use ConduitUI\Pr\QueryBuilder;
use Illuminate\Support\Facades\Facade;

/**
* @method static QueryBuilder for(string $repository)
* @method static PullRequest find(string $repository, int $number)
* @method static PullRequest create(string $repository, array $attributes)
* @method static QueryBuilder query()
* @method static QueryBuilder open(string $repository)
* @method static QueryBuilder closed(string $repository)
* @method static QueryBuilder merged(string $repository)
*
* @see \ConduitUI\Pr\PullRequests
*/
class PullRequests extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return \ConduitUI\Pr\PullRequests::class;
}
}
17 changes: 17 additions & 0 deletions src/PrServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ public function register(): void
$this->app->singleton(GitHubPrService::class, function ($app) {
return $app->make(PrServiceInterface::class);
});

// Register PullRequests for facade support
$this->app->singleton(PullRequests::class, function ($app) {
// Return a proxy that allows both static and instance usage
return new class($app->make(PrServiceInterface::class))
{
public function __construct(private PrServiceInterface $service)
{
PullRequests::setService($this->service);
}

public function __call(string $method, array $arguments): mixed
{
return PullRequests::$method(...$arguments);
}
};
});
}

/**
Expand Down
32 changes: 28 additions & 4 deletions src/PullRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ public static function query(): QueryBuilder
return self::service()->query();
}

/**
* Get open pull requests for a repository (shorthand for for()->open())
*/
public static function open(string $repository): QueryBuilder
{
return self::for($repository)->open();
}

/**
* Get closed pull requests for a repository (shorthand for for()->closed())
*/
public static function closed(string $repository): QueryBuilder
{
return self::for($repository)->closed();
}

/**
* Get merged pull requests for a repository
*/
public static function merged(string $repository): QueryBuilder
{
return self::service()->for($repository)->state('closed');
}

/**
* Get a pull request by number
*/
Expand Down Expand Up @@ -150,21 +174,21 @@ public function list(array $filters = []): array
}

/**
* Get only open pull requests
* Get only open pull requests (instance method)
*
* @return array<int, PullRequest>
*/
public function open(): array
public function listOpen(): array
{
return $this->list(['state' => 'open']);
}

/**
* Get only closed pull requests
* Get only closed pull requests (instance method)
*
* @return array<int, PullRequest>
*/
public function closed(): array
public function listClosed(): array
{
return $this->list(['state' => 'closed']);
}
Expand Down
201 changes: 201 additions & 0 deletions tests/Unit/Facades/PullRequestsFacadeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

declare(strict_types=1);

use ConduitUi\GitHubConnector\Connector;
use ConduitUI\Pr\Facades\PullRequests as PullRequestsFacade;
use ConduitUI\Pr\PullRequests;
use ConduitUI\Pr\QueryBuilder;
use ConduitUI\Pr\Services\GitHubPrService;
use Illuminate\Support\Facades\Facade;
use Saloon\Http\Request;
use Saloon\Http\Response;

class LaravelFacadeMockResponse extends Response
{
/**
* @param array<string, mixed> $data
*/
public function __construct(private array $data)
{
// Skip parent constructor
}

public function json(string|int|null $key = null, mixed $default = null): mixed
{
if ($key !== null) {
return $this->data[$key] ?? $default;
}

return $this->data;
}
}

class LaravelFacadeTestConnector extends Connector
{
private int $callIndex = 0;

/**
* @var array<int, array<string, mixed>>
*/
protected array $mockResponses = [];

/**
* @param array<int, array<string, mixed>> $responses
*/
public function __construct(array $responses = [])
{
parent::__construct('test-token');
$this->mockResponses = $responses;
}

public function send(Request $request, ...$args): Response
{
$response = $this->mockResponses[$this->callIndex++] ?? [];

return new LaravelFacadeMockResponse($response);
}
}

function createLaravelLaravelFacadeTestConnector(array $responses = []): Connector
{
return new LaravelFacadeTestConnector($responses);
}

function createLaravelFacadeMockPrData(): array
{
return [
'number' => 1,
'title' => 'Test PR',
'body' => 'Test body',
'state' => 'open',
'user' => [
'id' => 1,
'login' => 'testuser',
'avatar_url' => 'https://example.com/avatar.jpg',
'html_url' => 'https://github.com/testuser',
'type' => 'User',
],
'html_url' => 'https://github.com/owner/repo/pull/1',
'created_at' => '2025-01-01T00:00:00Z',
'updated_at' => '2025-01-01T00:00:00Z',
'draft' => false,
'merged' => false,
'head' => [
'ref' => 'feature',
'sha' => 'abc123',
'user' => [
'id' => 1,
'login' => 'testuser',
'avatar_url' => 'https://example.com/avatar.jpg',
'html_url' => 'https://github.com/testuser',
'type' => 'User',
],
'repo' => [
'id' => 1,
'name' => 'repo',
'full_name' => 'owner/repo',
'html_url' => 'https://github.com/owner/repo',
'private' => false,
],
],
'base' => [
'ref' => 'main',
'sha' => 'def456',
'user' => [
'id' => 1,
'login' => 'testuser',
'avatar_url' => 'https://example.com/avatar.jpg',
'html_url' => 'https://github.com/testuser',
'type' => 'User',
],
'repo' => [
'id' => 1,
'name' => 'repo',
'full_name' => 'owner/repo',
'html_url' => 'https://github.com/owner/repo',
'private' => false,
],
],
];
}

beforeEach(function () {
// Set up the service for the facade
$connector = createLaravelLaravelFacadeTestConnector([[createLaravelFacadeMockPrData()]]);
$service = new GitHubPrService($connector);

// Configure the underlying PullRequests class
PullRequests::setService($service);
});

it('extends Laravel Facade', function () {
expect(PullRequestsFacade::class)->toExtend(Facade::class);
});

it('has correct facade accessor', function () {
expect((new ReflectionClass(PullRequestsFacade::class))
->getMethod('getFacadeAccessor')
->invoke(null))->toBe(PullRequests::class);
});

it('provides static open() shorthand method', function () {
$query = PullRequests::open('owner/repo');

expect($query)->toBeInstanceOf(QueryBuilder::class);
});

it('provides static closed() shorthand method', function () {
$query = PullRequests::closed('owner/repo');

expect($query)->toBeInstanceOf(QueryBuilder::class);
});

it('provides static merged() shorthand method', function () {
$connector = createLaravelLaravelFacadeTestConnector([[array_merge(createLaravelFacadeMockPrData(), ['merged' => true])]]);
$service = new GitHubPrService($connector);
PullRequests::setService($service);

$query = PullRequests::merged('owner/repo');

expect($query)->toBeInstanceOf(QueryBuilder::class);
});

it('allows method chaining with shorthand methods', function () {
$query = PullRequests::open('owner/repo')
->author('testuser')
->label('bug');

expect($query)->toBeInstanceOf(QueryBuilder::class);
});

it('open() returns query builder with open state', function () {
$connector = createLaravelLaravelFacadeTestConnector([[createLaravelFacadeMockPrData()]]);
$service = new GitHubPrService($connector);
PullRequests::setService($service);

$query = PullRequests::open('owner/repo');
$results = $query->get();

expect($results)->toBeArray();
});

it('closed() returns query builder with closed state', function () {
$connector = createLaravelLaravelFacadeTestConnector([[createLaravelFacadeMockPrData()]]);
$service = new GitHubPrService($connector);
PullRequests::setService($service);

$query = PullRequests::closed('owner/repo');

expect($query)->toBeInstanceOf(QueryBuilder::class);
});

it('merged() returns query builder for merged PRs', function () {
$connector = createLaravelLaravelFacadeTestConnector([[array_merge(createLaravelFacadeMockPrData(), ['merged' => true, 'state' => 'closed'])]]);
$service = new GitHubPrService($connector);
PullRequests::setService($service);

$query = PullRequests::merged('owner/repo');

expect($query)->toBeInstanceOf(QueryBuilder::class);
});
8 changes: 4 additions & 4 deletions tests/Unit/PullRequestsFacadeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,20 +209,20 @@ function createFacadeMockPrData(): array
->and($results[0])->toBeInstanceOf(PullRequest::class);
});

it('instance open returns open pull requests', function () {
it('instance listOpen returns open pull requests', function () {
$connector = createFacadeTestConnector([[createFacadeMockPrData()]]);
$prs = new PullRequests($connector, 'owner', 'repo');

$results = $prs->open();
$results = $prs->listOpen();

expect($results)->toBeArray();
});

it('instance closed returns closed pull requests', function () {
it('instance listClosed returns closed pull requests', function () {
$connector = createFacadeTestConnector([[createFacadeMockPrData()]]);
$prs = new PullRequests($connector, 'owner', 'repo');

$results = $prs->closed();
$results = $prs->listClosed();

expect($results)->toBeArray();
});
Expand Down
Loading